@colyseus/schema 5.0.10 → 5.0.12
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/Metadata.d.ts +20 -12
- package/build/annotations.d.ts +23 -10
- package/build/codegen/cli.cjs +615 -204
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/dart.d.ts +20 -0
- package/build/codegen/types.d.ts +20 -0
- package/build/decoder/Resync.d.ts +3 -3
- package/build/encoder/ChangeTree.d.ts +22 -10
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +393 -287
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +393 -287
- package/build/index.mjs +392 -286
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/StreamSchema.d.ts +1 -1
- package/build/types/symbols.d.ts +4 -10
- package/package.json +1 -1
- package/src/Metadata.ts +58 -31
- package/src/annotations.ts +56 -32
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/c.ts +21 -3
- package/src/codegen/languages/csharp.ts +7 -1
- package/src/codegen/languages/dart.ts +274 -0
- package/src/codegen/languages/haxe.ts +7 -1
- package/src/codegen/languages/lua.ts +16 -4
- package/src/codegen/languages/ts.ts +5 -0
- package/src/codegen/parser.ts +97 -3
- package/src/codegen/types.ts +24 -0
- package/src/decoder/Resync.ts +8 -8
- package/src/encoder/ChangeRecorder.ts +1 -1
- package/src/encoder/ChangeTree.ts +41 -28
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +100 -37
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +118 -47
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/streaming.ts +8 -0
- package/src/encoding/spec.ts +1 -1
- package/src/index.ts +2 -2
- package/src/types/builder.ts +35 -31
- package/src/types/custom/StreamSchema.ts +1 -1
- package/src/types/symbols.ts +4 -11
- package/src/bench_bloat.ts +0 -173
- package/src/bench_churn.ts +0 -121
- package/src/bench_decode.ts +0 -221
- package/src/bench_decode_mem.ts +0 -165
- package/src/bench_encode.ts +0 -108
- package/src/bench_init.ts +0 -150
- package/src/bench_static.ts +0 -109
- package/src/bench_stream.ts +0 -295
- package/src/bench_view_cmp.ts +0 -142
package/build/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const SWITCH_TO_STRUCTURE = 255; //
|
|
3
|
+
const SWITCH_TO_STRUCTURE = 255; // same byte as `DELETE_AND_ADD | 63`, which is why field index 63 is unassignable (Metadata.MAX_FIELDS)
|
|
4
4
|
const TYPE_ID = 213;
|
|
5
5
|
/**
|
|
6
6
|
* Encoding Schema field operations.
|
|
@@ -120,16 +120,10 @@ const $builder = "~builder";
|
|
|
120
120
|
* Metadata
|
|
121
121
|
*/
|
|
122
122
|
const $descriptors = "~descriptors";
|
|
123
|
-
/**
|
|
124
|
-
* Per-class bitmask: bit i set iff field i carries a @view tag.
|
|
125
|
-
* Lazily computed from $viewFieldIndexes on first encode pass.
|
|
126
|
-
* Skips the per-field metadata[i].tag property chase in the hot encode loop.
|
|
127
|
-
*/
|
|
128
|
-
const $filterBitmask = "~__filterBitmask";
|
|
129
123
|
/**
|
|
130
124
|
* Cached per-class encode descriptor: bundles encoder fn, filter fn,
|
|
131
|
-
* metadata, isSchema flag
|
|
132
|
-
* the constructor. Replaces
|
|
125
|
+
* metadata, isSchema flag and the per-field arrays into one object stashed
|
|
126
|
+
* on the constructor. Replaces several separate per-tree property chases /
|
|
133
127
|
* function calls in the encode loop with a single property load.
|
|
134
128
|
*/
|
|
135
129
|
const $encodeDescriptor = "~__encodeDescriptor";
|
|
@@ -139,8 +133,8 @@ const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
|
|
|
139
133
|
const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
140
134
|
const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
141
135
|
const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
142
|
-
const $
|
|
143
|
-
const $
|
|
136
|
+
const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
137
|
+
const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
144
138
|
const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
145
139
|
const $streamPriorities = "~__streamPriorities";
|
|
146
140
|
|
|
@@ -821,6 +815,7 @@ function streamDropView(s, viewId) {
|
|
|
821
815
|
return;
|
|
822
816
|
st.pendingByView.delete(viewId);
|
|
823
817
|
st.sentByView.delete(viewId);
|
|
818
|
+
st.priorityByView?.delete(viewId);
|
|
824
819
|
}
|
|
825
820
|
|
|
826
821
|
const WIRE_BY_BITS = {
|
|
@@ -1066,6 +1061,15 @@ class TypeContext {
|
|
|
1066
1061
|
}
|
|
1067
1062
|
}
|
|
1068
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Field indexes ride in the low 6 bits of the operation byte
|
|
1066
|
+
* (`(index | operation) & 255`), which leaves room for 0..63. Index 63 is
|
|
1067
|
+
* given up: `DELETE_AND_ADD | 63` is 255, the same byte the decoder claims
|
|
1068
|
+
* as SWITCH_TO_STRUCTURE before any field decoder sees it. Every nullable
|
|
1069
|
+
* field can produce that operation (delete-then-set in one tick merges to
|
|
1070
|
+
* DELETE_AND_ADD), so the slot is unusable rather than partly usable.
|
|
1071
|
+
*/
|
|
1072
|
+
const MAX_FIELDS = 63;
|
|
1069
1073
|
/**
|
|
1070
1074
|
* Given a normalized field type (`"number"`, `{ map: Foo }`, `Player`,
|
|
1071
1075
|
* etc.), split into the collection-type descriptor (`{ constructor:
|
|
@@ -1129,10 +1133,12 @@ function isTSEnum(_enum) {
|
|
|
1129
1133
|
}
|
|
1130
1134
|
const Metadata = {
|
|
1131
1135
|
addField(metadata, index, name, type, descriptor) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1136
|
+
// `index` is 0-based, so 62 is the last usable slot — see MAX_FIELDS
|
|
1137
|
+
// for why 63 is off limits.
|
|
1138
|
+
if (index >= MAX_FIELDS) {
|
|
1139
|
+
throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to ${MAX_FIELDS} fields.`);
|
|
1134
1140
|
}
|
|
1135
|
-
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@
|
|
1141
|
+
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@deprecated / @unreliable)
|
|
1136
1142
|
{
|
|
1137
1143
|
type: getNormalizedType(type),
|
|
1138
1144
|
index,
|
|
@@ -1282,31 +1288,43 @@ const Metadata = {
|
|
|
1282
1288
|
}
|
|
1283
1289
|
metadata[$unreliableFieldIndexes].push(index);
|
|
1284
1290
|
},
|
|
1285
|
-
|
|
1291
|
+
setPatchOnly(metadata, fieldName) {
|
|
1286
1292
|
const index = metadata[fieldName];
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1293
|
+
// patchOnly + fullStateOnly are the only two delivery channels —
|
|
1294
|
+
// excluding a field from both would silently never reach a client.
|
|
1295
|
+
// (The builder validates earlier; this guards the decorator path.)
|
|
1296
|
+
if (metadata[index].fullStateOnly) {
|
|
1297
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1298
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1299
|
+
}
|
|
1300
|
+
metadata[index].patchOnly = true;
|
|
1301
|
+
if (!metadata[$patchOnlyFieldIndexes]) {
|
|
1302
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1290
1303
|
value: [],
|
|
1291
1304
|
enumerable: false,
|
|
1292
1305
|
configurable: true,
|
|
1293
1306
|
writable: true,
|
|
1294
1307
|
});
|
|
1295
1308
|
}
|
|
1296
|
-
metadata[$
|
|
1309
|
+
metadata[$patchOnlyFieldIndexes].push(index);
|
|
1297
1310
|
},
|
|
1298
|
-
|
|
1311
|
+
setFullStateOnly(metadata, fieldName) {
|
|
1299
1312
|
const index = metadata[fieldName];
|
|
1300
|
-
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1313
|
+
// Mirror of the guard in setPatchOnly — covers both decorator orders.
|
|
1314
|
+
if (metadata[index].patchOnly) {
|
|
1315
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1316
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1317
|
+
}
|
|
1318
|
+
metadata[index].fullStateOnly = true;
|
|
1319
|
+
if (!metadata[$fullStateOnlyFieldIndexes]) {
|
|
1320
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1303
1321
|
value: [],
|
|
1304
1322
|
enumerable: false,
|
|
1305
1323
|
configurable: true,
|
|
1306
1324
|
writable: true,
|
|
1307
1325
|
});
|
|
1308
1326
|
}
|
|
1309
|
-
metadata[$
|
|
1327
|
+
metadata[$fullStateOnlyFieldIndexes].push(index);
|
|
1310
1328
|
},
|
|
1311
1329
|
setStream(metadata, fieldName) {
|
|
1312
1330
|
const index = metadata[fieldName];
|
|
@@ -1470,19 +1488,19 @@ const Metadata = {
|
|
|
1470
1488
|
writable: true,
|
|
1471
1489
|
});
|
|
1472
1490
|
}
|
|
1473
|
-
// $
|
|
1474
|
-
if (parentMetadata[$
|
|
1475
|
-
Object.defineProperty(metadata, $
|
|
1476
|
-
value: [...parentMetadata[$
|
|
1491
|
+
// $patchOnlyFieldIndexes
|
|
1492
|
+
if (parentMetadata[$patchOnlyFieldIndexes] !== undefined) {
|
|
1493
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1494
|
+
value: [...parentMetadata[$patchOnlyFieldIndexes]],
|
|
1477
1495
|
enumerable: false,
|
|
1478
1496
|
configurable: true,
|
|
1479
1497
|
writable: true,
|
|
1480
1498
|
});
|
|
1481
1499
|
}
|
|
1482
|
-
// $
|
|
1483
|
-
if (parentMetadata[$
|
|
1484
|
-
Object.defineProperty(metadata, $
|
|
1485
|
-
value: [...parentMetadata[$
|
|
1500
|
+
// $fullStateOnlyFieldIndexes
|
|
1501
|
+
if (parentMetadata[$fullStateOnlyFieldIndexes] !== undefined) {
|
|
1502
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1503
|
+
value: [...parentMetadata[$fullStateOnlyFieldIndexes]],
|
|
1486
1504
|
enumerable: false,
|
|
1487
1505
|
configurable: true,
|
|
1488
1506
|
writable: true,
|
|
@@ -1540,11 +1558,11 @@ const Metadata = {
|
|
|
1540
1558
|
hasUnreliableAtIndex(metadata, index) {
|
|
1541
1559
|
return metadata?.[$unreliableFieldIndexes]?.includes(index);
|
|
1542
1560
|
},
|
|
1543
|
-
|
|
1544
|
-
return metadata?.[$
|
|
1561
|
+
hasPatchOnlyAtIndex(metadata, index) {
|
|
1562
|
+
return metadata?.[$patchOnlyFieldIndexes]?.includes(index);
|
|
1545
1563
|
},
|
|
1546
|
-
|
|
1547
|
-
return metadata?.[$
|
|
1564
|
+
hasFullStateOnlyAtIndex(metadata, index) {
|
|
1565
|
+
return metadata?.[$fullStateOnlyFieldIndexes]?.includes(index);
|
|
1548
1566
|
},
|
|
1549
1567
|
hasStreamAtIndex(metadata, index) {
|
|
1550
1568
|
return metadata?.[$streamFieldIndexes]?.includes(index);
|
|
@@ -1555,7 +1573,7 @@ const Metadata = {
|
|
|
1555
1573
|
// by passing the user's callback as ctx. No per-call allocation.
|
|
1556
1574
|
const _invokeNoCtx$2 = (cb, index, op) => cb(index, op);
|
|
1557
1575
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1558
|
-
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤
|
|
1576
|
+
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤63 fields)
|
|
1559
1577
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1560
1578
|
/**
|
|
1561
1579
|
* Schema field operations are limited to ADD(128), DELETE(64), and
|
|
@@ -1748,37 +1766,15 @@ function popcount32(n) {
|
|
|
1748
1766
|
* ctor[$filter]
|
|
1749
1767
|
* ctor[Symbol.metadata]
|
|
1750
1768
|
* Metadata.isValidInstance(ref)
|
|
1751
|
-
* getFilterBitmask(metadata)
|
|
1752
1769
|
*
|
|
1753
1770
|
* Lives in its own file to break the Encoder.ts ↔ ChangeTree.ts import
|
|
1754
1771
|
* cycle (ChangeTree caches descriptors at construction; Encoder reads them
|
|
1755
1772
|
* during encode).
|
|
1756
1773
|
*/
|
|
1757
|
-
function computeFilterBitmask(metadata) {
|
|
1758
|
-
if (metadata === undefined)
|
|
1759
|
-
return 0;
|
|
1760
|
-
let bm = metadata[$filterBitmask];
|
|
1761
|
-
if (bm !== undefined)
|
|
1762
|
-
return bm;
|
|
1763
|
-
bm = 0;
|
|
1764
|
-
const tagged = metadata[$viewFieldIndexes];
|
|
1765
|
-
if (tagged !== undefined) {
|
|
1766
|
-
for (let i = 0, len = tagged.length; i < len; i++)
|
|
1767
|
-
bm |= (1 << tagged[i]);
|
|
1768
|
-
}
|
|
1769
|
-
// Non-enumerable so `for (const k in metadata)` iteration in TypeContext
|
|
1770
|
-
// and elsewhere doesn't mistake this cache for a real field index.
|
|
1771
|
-
Object.defineProperty(metadata, $filterBitmask, {
|
|
1772
|
-
value: bm,
|
|
1773
|
-
enumerable: false,
|
|
1774
|
-
writable: true,
|
|
1775
|
-
configurable: true,
|
|
1776
|
-
});
|
|
1777
|
-
return bm;
|
|
1778
|
-
}
|
|
1779
1774
|
/**
|
|
1780
1775
|
* Bitmask of field indexes 0–31 in `indexes`. For fields ≥32 callers must
|
|
1781
|
-
* fall back to the array lookup
|
|
1776
|
+
* fall back to the array lookup — shift counts wrap at 32, so an unguarded
|
|
1777
|
+
* `1 << 40` would set bit 8 and misclassify field 8.
|
|
1782
1778
|
*/
|
|
1783
1779
|
function indexesToBitmask(indexes) {
|
|
1784
1780
|
if (indexes === undefined)
|
|
@@ -1854,12 +1850,12 @@ function getEncodeDescriptor(ref) {
|
|
|
1854
1850
|
filter,
|
|
1855
1851
|
metadata,
|
|
1856
1852
|
isSchema,
|
|
1857
|
-
filterBitmask: isSchema ?
|
|
1858
|
-
|
|
1853
|
+
filterBitmask: isSchema ? indexesToBitmask(metadata?.[$viewFieldIndexes]) : 0,
|
|
1854
|
+
hasAnyFullStateOnly: (metadata?.[$fullStateOnlyFieldIndexes]?.length ?? 0) > 0,
|
|
1859
1855
|
hasAnyUnreliable: (metadata?.[$unreliableFieldIndexes]?.length ?? 0) > 0,
|
|
1860
1856
|
hasAnyStream: (metadata?.[$streamFieldIndexes]?.length ?? 0) > 0,
|
|
1861
1857
|
hasAnyView,
|
|
1862
|
-
|
|
1858
|
+
fullStateOnlyBitmask: indexesToBitmask(metadata?.[$fullStateOnlyFieldIndexes]),
|
|
1863
1859
|
unreliableBitmask: indexesToBitmask(metadata?.[$unreliableFieldIndexes]),
|
|
1864
1860
|
streamBitmask: indexesToBitmask(metadata?.[$streamFieldIndexes]),
|
|
1865
1861
|
names: arrays.names,
|
|
@@ -1995,13 +1991,13 @@ function getAllParents(tree) {
|
|
|
1995
1991
|
}
|
|
1996
1992
|
|
|
1997
1993
|
/**
|
|
1998
|
-
* Walk all currently-populated non-
|
|
1994
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
1999
1995
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
2000
1996
|
* StateView.add to derive full-sync output from the live structure.
|
|
2001
1997
|
*
|
|
2002
|
-
*
|
|
1998
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
2003
1999
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
2004
|
-
* field is @
|
|
2000
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
2005
2001
|
*/
|
|
2006
2002
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
2007
2003
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
@@ -2015,10 +2011,10 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2015
2011
|
// types. See `ChangeTree.refTarget` doc.
|
|
2016
2012
|
const ref = tree.refTarget;
|
|
2017
2013
|
if (ref[$childType] !== undefined) {
|
|
2018
|
-
// Collection inheriting @
|
|
2014
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
2019
2015
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
2020
2016
|
// absent from full-sync output is never pruned client-side.
|
|
2021
|
-
if (tree.
|
|
2017
|
+
if (tree.isPatchOnly)
|
|
2022
2018
|
return;
|
|
2023
2019
|
// Collection types: dispatch by shape.
|
|
2024
2020
|
if (Array.isArray(ref.items)) {
|
|
@@ -2047,7 +2043,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2047
2043
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
2048
2044
|
// the setter records a DELETE when a field is set to null or
|
|
2049
2045
|
// undefined, so it should not appear in full-sync output.
|
|
2050
|
-
// (@
|
|
2046
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
2051
2047
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
2052
2048
|
//
|
|
2053
2049
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -2057,13 +2053,13 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2057
2053
|
if (!metadata)
|
|
2058
2054
|
return;
|
|
2059
2055
|
const numFields = (metadata[$numFields] ?? -1);
|
|
2060
|
-
const
|
|
2056
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
2061
2057
|
const names = tree.encDescriptor.names;
|
|
2062
2058
|
for (let i = 0; i <= numFields; i++) {
|
|
2063
2059
|
const name = names[i];
|
|
2064
2060
|
if (name === undefined)
|
|
2065
2061
|
continue;
|
|
2066
|
-
if (
|
|
2062
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i))
|
|
2067
2063
|
continue;
|
|
2068
2064
|
const value = ref[name];
|
|
2069
2065
|
if (value !== undefined && value !== null)
|
|
@@ -2073,7 +2069,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2073
2069
|
}
|
|
2074
2070
|
|
|
2075
2071
|
/**
|
|
2076
|
-
* Filter / unreliable /
|
|
2072
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
2077
2073
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
2078
2074
|
* the parent field's annotation + the parent tree's own state.
|
|
2079
2075
|
*/
|
|
@@ -2086,7 +2082,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2086
2082
|
checkInheritedFlags(tree, parent, parentIndex);
|
|
2087
2083
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
2088
2084
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
2089
|
-
if (tree.
|
|
2085
|
+
if (tree.isFullStateOnly)
|
|
2090
2086
|
return;
|
|
2091
2087
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
2092
2088
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -2112,7 +2108,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2112
2108
|
}
|
|
2113
2109
|
}
|
|
2114
2110
|
/**
|
|
2115
|
-
* Inherit filter / unreliable /
|
|
2111
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
2116
2112
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
2117
2113
|
* etc.) inherit these from the Schema field that holds them.
|
|
2118
2114
|
*
|
|
@@ -2151,14 +2147,14 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2151
2147
|
parentIndex = parentChangeTree.parentIndex;
|
|
2152
2148
|
}
|
|
2153
2149
|
const parentMetadata = parent?.constructor?.[Symbol.metadata];
|
|
2154
|
-
// Flag inheritance — pack the
|
|
2150
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
2155
2151
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
2156
2152
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
2157
2153
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
2158
2154
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
2159
2155
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
2160
|
-
const fieldBits = (parentMetadata?.[$
|
|
2161
|
-
| (parentMetadata?.[$
|
|
2156
|
+
const fieldBits = (parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
2157
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
2162
2158
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
2163
2159
|
const beforeFlags = tree.flags;
|
|
2164
2160
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -2168,7 +2164,7 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2168
2164
|
// `new Config().assign({...})` populates the recorder before the
|
|
2169
2165
|
// Config instance is attached). Static trees ship state via structural
|
|
2170
2166
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
2171
|
-
if (gainedBits &
|
|
2167
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
2172
2168
|
tree.reset();
|
|
2173
2169
|
tree.unreliableRecorder?.reset();
|
|
2174
2170
|
}
|
|
@@ -2473,7 +2469,7 @@ function _setParentChildCb(ctx, child, index) {
|
|
|
2473
2469
|
*
|
|
2474
2470
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
2475
2471
|
* - liveIteration.ts forEachLive
|
|
2476
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
2472
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
2477
2473
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
2478
2474
|
*
|
|
2479
2475
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -2493,12 +2489,12 @@ function readInlineOpByte(low, high, index) {
|
|
|
2493
2489
|
const _invokeNoCtx = (cb, index, op) => cb(index, op);
|
|
2494
2490
|
// Linked list helper functions
|
|
2495
2491
|
function createChangeTreeList() {
|
|
2496
|
-
return { next: undefined, tail: undefined };
|
|
2492
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
2497
2493
|
}
|
|
2498
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
2494
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
2499
2495
|
// field's annotation — inherited at setParent/setRoot time.
|
|
2500
2496
|
const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
2501
|
-
const IS_UNRELIABLE = 8,
|
|
2497
|
+
const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
2502
2498
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
2503
2499
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
2504
2500
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -2525,7 +2521,7 @@ const NEEDS_RESTAGE = 128;
|
|
|
2525
2521
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
2526
2522
|
* field mutations only) is designed later.
|
|
2527
2523
|
*/
|
|
2528
|
-
const INHERITABLE_FLAGS =
|
|
2524
|
+
const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
2529
2525
|
class ChangeTree {
|
|
2530
2526
|
ref;
|
|
2531
2527
|
/**
|
|
@@ -2544,8 +2540,8 @@ class ChangeTree {
|
|
|
2544
2540
|
refTarget;
|
|
2545
2541
|
metadata;
|
|
2546
2542
|
/**
|
|
2547
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
2548
|
-
*
|
|
2543
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
2544
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
2549
2545
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
2550
2546
|
* EncodeDescriptor.ts.
|
|
2551
2547
|
*/
|
|
@@ -2594,7 +2590,6 @@ class ChangeTree {
|
|
|
2594
2590
|
// per-view WeakSet lookups with direct bitwise ops.
|
|
2595
2591
|
// Lazy: undefined until the tree participates in any view.
|
|
2596
2592
|
visibleViews;
|
|
2597
|
-
invisibleViews;
|
|
2598
2593
|
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
2599
2594
|
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
2600
2595
|
tagViews;
|
|
@@ -2617,10 +2612,10 @@ class ChangeTree {
|
|
|
2617
2612
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2618
2613
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2619
2614
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2620
|
-
get
|
|
2621
|
-
set
|
|
2622
|
-
get
|
|
2623
|
-
set
|
|
2615
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
2616
|
+
set isPatchOnly(v) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
2617
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
2618
|
+
set isFullStateOnly(v) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
2624
2619
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2625
2620
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2626
2621
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2629,7 +2624,7 @@ class ChangeTree {
|
|
|
2629
2624
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2630
2625
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2631
2626
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2632
|
-
// pattern as `
|
|
2627
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2633
2628
|
get hasFilteredFields() {
|
|
2634
2629
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2635
2630
|
}
|
|
@@ -2653,7 +2648,7 @@ class ChangeTree {
|
|
|
2653
2648
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2654
2649
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2655
2650
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2656
|
-
// (
|
|
2651
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2657
2652
|
const desc = this.encDescriptor;
|
|
2658
2653
|
if (!desc.hasAnyUnreliable)
|
|
2659
2654
|
return false;
|
|
@@ -2663,15 +2658,15 @@ class ChangeTree {
|
|
|
2663
2658
|
}
|
|
2664
2659
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2665
2660
|
// by the tracker (the value still lives on the instance).
|
|
2666
|
-
|
|
2667
|
-
if (this.
|
|
2661
|
+
isFieldFullStateOnly(index) {
|
|
2662
|
+
if (this.isFullStateOnly)
|
|
2668
2663
|
return true;
|
|
2669
2664
|
const desc = this.encDescriptor;
|
|
2670
|
-
if (!desc.
|
|
2665
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2671
2666
|
return false;
|
|
2672
2667
|
if (index < 32)
|
|
2673
|
-
return (desc.
|
|
2674
|
-
return Metadata.
|
|
2668
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2669
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2675
2670
|
}
|
|
2676
2671
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2677
2672
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2930,7 +2925,7 @@ class ChangeTree {
|
|
|
2930
2925
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2931
2926
|
this.unreliableRecorder?.reset();
|
|
2932
2927
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2933
|
-
// (FILTERED/
|
|
2928
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2934
2929
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2935
2930
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2936
2931
|
this._fullSyncGen = 0;
|
|
@@ -2946,7 +2941,6 @@ class ChangeTree {
|
|
|
2946
2941
|
// per-view visibility lives on the tree (NOT keyed by refId), so a
|
|
2947
2942
|
// recycled tree must not inherit its previous life's view membership.
|
|
2948
2943
|
this.visibleViews = undefined;
|
|
2949
|
-
this.invisibleViews = undefined;
|
|
2950
2944
|
this.tagViews = undefined;
|
|
2951
2945
|
this.subscribedViews = undefined;
|
|
2952
2946
|
}
|
|
@@ -2964,7 +2958,7 @@ class ChangeTree {
|
|
|
2964
2958
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2965
2959
|
const src = this.collDirty;
|
|
2966
2960
|
const dst = new Map();
|
|
2967
|
-
const track = !this.paused && !this.
|
|
2961
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2968
2962
|
if (track) {
|
|
2969
2963
|
for (let i = 0; i < count; i++)
|
|
2970
2964
|
dst.set(i, exports.OPERATION.ADD);
|
|
@@ -2988,7 +2982,7 @@ class ChangeTree {
|
|
|
2988
2982
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2989
2983
|
}
|
|
2990
2984
|
operation(op) {
|
|
2991
|
-
if (this.paused || this.
|
|
2985
|
+
if (this.paused || this.isFullStateOnly)
|
|
2992
2986
|
return;
|
|
2993
2987
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2994
2988
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3018,11 +3012,23 @@ class ChangeTree {
|
|
|
3018
3012
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3019
3013
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3020
3014
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
3015
|
+
*
|
|
3016
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
3017
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
3018
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
3019
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
3020
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
3021
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
3022
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
3023
|
+
* which has always seeded these fields for late joiners.
|
|
3024
|
+
*
|
|
3025
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
3026
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
3021
3027
|
*/
|
|
3022
3028
|
_routeAndRecord(index, op, raw) {
|
|
3023
|
-
if (this.paused || this.
|
|
3029
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3024
3030
|
return;
|
|
3025
|
-
if (this.isFieldUnreliable(index)) {
|
|
3031
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3026
3032
|
const r = this.ensureUnreliableRecorder();
|
|
3027
3033
|
if (raw)
|
|
3028
3034
|
r.recordRaw(index, op);
|
|
@@ -3089,9 +3095,11 @@ class ChangeTree {
|
|
|
3089
3095
|
}
|
|
3090
3096
|
return;
|
|
3091
3097
|
}
|
|
3092
|
-
if (this.paused || this.
|
|
3098
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3093
3099
|
return this.getValue(index);
|
|
3094
|
-
|
|
3100
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
3101
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
3102
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
3095
3103
|
if (unreliable)
|
|
3096
3104
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? exports.OPERATION.DELETE);
|
|
3097
3105
|
else
|
|
@@ -3297,7 +3305,9 @@ function encodeValue(encoder, bytes, type, value, operation, it, encoderFn) {
|
|
|
3297
3305
|
* @private
|
|
3298
3306
|
*/
|
|
3299
3307
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3300
|
-
// "compress" field index + operation
|
|
3308
|
+
// "compress" field index + operation. Can't collide with
|
|
3309
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
3310
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
3301
3311
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3302
3312
|
// Do not encode value for DELETE operations
|
|
3303
3313
|
if (operation === exports.OPERATION.DELETE) {
|
|
@@ -3493,8 +3503,8 @@ function resyncTouchEntry(decoder, ref, operation, identity, previousValue, valu
|
|
|
3493
3503
|
/**
|
|
3494
3504
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3495
3505
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3496
|
-
* of full-sync" (@
|
|
3497
|
-
* live data. Reflected clients have no @
|
|
3506
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
3507
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
3498
3508
|
* presence is the only reliable signal.
|
|
3499
3509
|
*/
|
|
3500
3510
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3508,7 +3518,7 @@ function resyncMarkPresent(decoder, refId) {
|
|
|
3508
3518
|
* entry the snapshot did not visit.
|
|
3509
3519
|
*
|
|
3510
3520
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3511
|
-
* `@
|
|
3521
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3512
3522
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3513
3523
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3514
3524
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3533,11 +3543,11 @@ function sweepSchema(decoder, ref, seen, allChanges) {
|
|
|
3533
3543
|
if (refIndexes === undefined) {
|
|
3534
3544
|
return;
|
|
3535
3545
|
}
|
|
3536
|
-
const
|
|
3546
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3537
3547
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3538
3548
|
const fieldIndex = refIndexes[i];
|
|
3539
|
-
// @
|
|
3540
|
-
if (
|
|
3549
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3550
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3541
3551
|
continue;
|
|
3542
3552
|
}
|
|
3543
3553
|
const field = metadata[fieldIndex];
|
|
@@ -3562,7 +3572,7 @@ function sweepCollection(decoder, coll, seen, allChanges) {
|
|
|
3562
3572
|
seen.add(refId);
|
|
3563
3573
|
// `undefined` = the collection never appeared in the payload at all
|
|
3564
3574
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3565
|
-
// (@
|
|
3575
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3566
3576
|
// means "present with zero entries" → prune everything.
|
|
3567
3577
|
const visited = decoder.resyncVisited.get(refId);
|
|
3568
3578
|
if (visited === undefined) {
|
|
@@ -5881,7 +5891,7 @@ registerType("set", { constructor: SetSchema });
|
|
|
5881
5891
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5882
5892
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5883
5893
|
* propagate through the normal reliable channel without consuming the
|
|
5884
|
-
* per-tick budget. Chain `.
|
|
5894
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5885
5895
|
* post-add mutation tracking entirely.
|
|
5886
5896
|
*/
|
|
5887
5897
|
class StreamSchema {
|
|
@@ -6156,12 +6166,11 @@ class FieldBuilder {
|
|
|
6156
6166
|
_default = undefined;
|
|
6157
6167
|
_hasDefault = false;
|
|
6158
6168
|
_view = undefined;
|
|
6159
|
-
_owned = false;
|
|
6160
6169
|
_unreliable = false;
|
|
6161
|
-
|
|
6170
|
+
_patchOnly = false;
|
|
6162
6171
|
_deprecated = false;
|
|
6163
6172
|
_deprecatedThrows = true;
|
|
6164
|
-
|
|
6173
|
+
_fullStateOnly = false;
|
|
6165
6174
|
_stream = false;
|
|
6166
6175
|
_optional = false;
|
|
6167
6176
|
_noSync = false;
|
|
@@ -6196,38 +6205,43 @@ class FieldBuilder {
|
|
|
6196
6205
|
this._view = tag ?? -1;
|
|
6197
6206
|
return this;
|
|
6198
6207
|
}
|
|
6199
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6200
|
-
owned() {
|
|
6201
|
-
this._owned = true;
|
|
6202
|
-
return this;
|
|
6203
|
-
}
|
|
6204
6208
|
/**
|
|
6205
6209
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6206
6210
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6207
|
-
* tagged with `.
|
|
6211
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
6212
|
+
*
|
|
6213
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
6214
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
6215
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
6216
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
6217
|
+
* the field is never written again.
|
|
6208
6218
|
*/
|
|
6209
6219
|
unreliable() {
|
|
6210
6220
|
this._unreliable = true;
|
|
6211
6221
|
return this;
|
|
6212
6222
|
}
|
|
6213
6223
|
/**
|
|
6214
|
-
*
|
|
6215
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6216
|
-
* only after its next mutation is emitted on a
|
|
6217
|
-
* to `.unreliable()`.
|
|
6224
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
6225
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6226
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
6227
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
6218
6228
|
*/
|
|
6219
|
-
|
|
6220
|
-
this.
|
|
6229
|
+
patchOnly() {
|
|
6230
|
+
this._patchOnly = true;
|
|
6221
6231
|
return this;
|
|
6222
6232
|
}
|
|
6223
6233
|
/**
|
|
6224
|
-
*
|
|
6225
|
-
*
|
|
6226
|
-
*
|
|
6227
|
-
*
|
|
6234
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
6235
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
6236
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
6237
|
+
* The mirror of `.patchOnly()`.
|
|
6238
|
+
*
|
|
6239
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
6240
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
6241
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
6228
6242
|
*/
|
|
6229
|
-
|
|
6230
|
-
this.
|
|
6243
|
+
fullStateOnly() {
|
|
6244
|
+
this._fullStateOnly = true;
|
|
6231
6245
|
return this;
|
|
6232
6246
|
}
|
|
6233
6247
|
/**
|
|
@@ -6239,8 +6253,8 @@ class FieldBuilder {
|
|
|
6239
6253
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6240
6254
|
* want on the class for typing convenience without paying any sync cost.
|
|
6241
6255
|
*
|
|
6242
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6243
|
-
* `.unreliable()`, `.
|
|
6256
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6257
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6244
6258
|
* them throws at `schema()` time.
|
|
6245
6259
|
*
|
|
6246
6260
|
* ```ts
|
|
@@ -6282,9 +6296,12 @@ class FieldBuilder {
|
|
|
6282
6296
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6283
6297
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6284
6298
|
*
|
|
6299
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
6300
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
6301
|
+
*
|
|
6285
6302
|
* ```ts
|
|
6286
6303
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6287
|
-
* -
|
|
6304
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6288
6305
|
* )
|
|
6289
6306
|
* ```
|
|
6290
6307
|
*/
|
|
@@ -6319,12 +6336,11 @@ class FieldBuilder {
|
|
|
6319
6336
|
default: this._default,
|
|
6320
6337
|
hasDefault: this._hasDefault,
|
|
6321
6338
|
view: this._view,
|
|
6322
|
-
owned: this._owned,
|
|
6323
6339
|
unreliable: this._unreliable,
|
|
6324
|
-
|
|
6340
|
+
patchOnly: this._patchOnly,
|
|
6325
6341
|
deprecated: this._deprecated,
|
|
6326
6342
|
deprecatedThrows: this._deprecatedThrows,
|
|
6327
|
-
|
|
6343
|
+
fullStateOnly: this._fullStateOnly,
|
|
6328
6344
|
stream: this._stream,
|
|
6329
6345
|
optional: this._optional,
|
|
6330
6346
|
noSync: this._noSync,
|
|
@@ -6578,25 +6594,44 @@ function view(tag = DEFAULT_VIEW_TAG) {
|
|
|
6578
6594
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6579
6595
|
};
|
|
6580
6596
|
}
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6597
|
+
/**
|
|
6598
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
6599
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
6600
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
6601
|
+
*
|
|
6602
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
6603
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
6604
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
6605
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
6606
|
+
* written again.
|
|
6607
|
+
*/
|
|
6585
6608
|
function unreliable(target, field) {
|
|
6586
6609
|
const metadata = Metadata.initialize(target.constructor);
|
|
6587
6610
|
Metadata.setUnreliable(metadata, field);
|
|
6588
6611
|
}
|
|
6589
6612
|
/**
|
|
6590
|
-
* @
|
|
6591
|
-
* encodeAllView).
|
|
6613
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6614
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6592
6615
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6593
6616
|
* the next mutation.
|
|
6594
6617
|
*
|
|
6595
6618
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6596
6619
|
*/
|
|
6597
|
-
function
|
|
6620
|
+
function patchOnly(target, field) {
|
|
6621
|
+
const metadata = Metadata.initialize(target.constructor);
|
|
6622
|
+
Metadata.setPatchOnly(metadata, field);
|
|
6623
|
+
}
|
|
6624
|
+
/**
|
|
6625
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
6626
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
6627
|
+
* client has joined are not propagated to it — populate these fields
|
|
6628
|
+
* before clients connect (e.g. during onCreate).
|
|
6629
|
+
*
|
|
6630
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
6631
|
+
*/
|
|
6632
|
+
function fullStateOnly(target, field) {
|
|
6598
6633
|
const metadata = Metadata.initialize(target.constructor);
|
|
6599
|
-
Metadata.
|
|
6634
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6600
6635
|
}
|
|
6601
6636
|
function type(type, options) {
|
|
6602
6637
|
return function (target, field) {
|
|
@@ -6968,11 +7003,10 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6968
7003
|
}
|
|
6969
7004
|
};
|
|
6970
7005
|
const viewTagFields = {};
|
|
6971
|
-
const ownedFields = [];
|
|
6972
7006
|
const unreliableFields = [];
|
|
6973
|
-
const
|
|
7007
|
+
const patchOnlyFields = [];
|
|
6974
7008
|
const deprecatedFields = {};
|
|
6975
|
-
const
|
|
7009
|
+
const fullStateOnlyFields = [];
|
|
6976
7010
|
const streamFields = [];
|
|
6977
7011
|
const streamPriorityFields = {};
|
|
6978
7012
|
const optionalFields = [];
|
|
@@ -6984,15 +7018,22 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6984
7018
|
// Local-only field: skip metadata registration entirely so it is
|
|
6985
7019
|
// never encoded/decoded, but still seed its construction default
|
|
6986
7020
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6987
|
-
if (def.view !== undefined || def.
|
|
6988
|
-
def.
|
|
7021
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7022
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6989
7023
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6990
|
-
`together with a sync-only modifier (.view/.
|
|
7024
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6991
7025
|
`A local-only field cannot be synchronized.`);
|
|
6992
7026
|
}
|
|
6993
7027
|
seedDefault(fieldName, def);
|
|
6994
7028
|
continue;
|
|
6995
7029
|
}
|
|
7030
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
7031
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
7032
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
7033
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
7034
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
7035
|
+
`so the field would never reach a client — use .noSync() if that is intended.`);
|
|
7036
|
+
}
|
|
6996
7037
|
const normalizedType = getNormalizedType(def.type);
|
|
6997
7038
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
6998
7039
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -7003,20 +7044,17 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
7003
7044
|
if (def.view !== undefined) {
|
|
7004
7045
|
viewTagFields[fieldName] = def.view;
|
|
7005
7046
|
}
|
|
7006
|
-
if (def.owned) {
|
|
7007
|
-
ownedFields.push(fieldName);
|
|
7008
|
-
}
|
|
7009
7047
|
if (def.unreliable) {
|
|
7010
7048
|
unreliableFields.push(fieldName);
|
|
7011
7049
|
}
|
|
7012
|
-
if (def.
|
|
7013
|
-
|
|
7050
|
+
if (def.patchOnly) {
|
|
7051
|
+
patchOnlyFields.push(fieldName);
|
|
7014
7052
|
}
|
|
7015
7053
|
if (def.deprecated) {
|
|
7016
7054
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7017
7055
|
}
|
|
7018
|
-
if (def.
|
|
7019
|
-
|
|
7056
|
+
if (def.fullStateOnly) {
|
|
7057
|
+
fullStateOnlyFields.push(fieldName);
|
|
7020
7058
|
}
|
|
7021
7059
|
if (def.stream) {
|
|
7022
7060
|
streamFields.push(fieldName);
|
|
@@ -7099,22 +7137,19 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
7099
7137
|
for (const fieldName in viewTagFields) {
|
|
7100
7138
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7101
7139
|
}
|
|
7102
|
-
for (const fieldName of ownedFields) {
|
|
7103
|
-
owned(klass.prototype, fieldName);
|
|
7104
|
-
}
|
|
7105
7140
|
for (const fieldName of unreliableFields) {
|
|
7106
7141
|
unreliable(klass.prototype, fieldName);
|
|
7107
7142
|
}
|
|
7108
|
-
for (const fieldName of
|
|
7109
|
-
|
|
7143
|
+
for (const fieldName of patchOnlyFields) {
|
|
7144
|
+
patchOnly(klass.prototype, fieldName);
|
|
7110
7145
|
}
|
|
7111
7146
|
for (const fieldName in deprecatedFields) {
|
|
7112
7147
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7113
7148
|
}
|
|
7114
|
-
if (
|
|
7149
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7115
7150
|
const metadata = klass[Symbol.metadata];
|
|
7116
|
-
for (const fieldName of
|
|
7117
|
-
Metadata.
|
|
7151
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7152
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7118
7153
|
}
|
|
7119
7154
|
for (const fieldName of streamFields) {
|
|
7120
7155
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7763,7 +7798,7 @@ class Root {
|
|
|
7763
7798
|
const previousRefCount = this.refCount[refId];
|
|
7764
7799
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7765
7800
|
//
|
|
7766
|
-
// Re-stage every currently-populated non-
|
|
7801
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7767
7802
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7768
7803
|
// re-emits it on the correct channel. Two triggers:
|
|
7769
7804
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7853,14 +7888,10 @@ class Root {
|
|
|
7853
7888
|
const parentNode = parent[$changes][nodeField];
|
|
7854
7889
|
if (!parentNode || parentNode === node)
|
|
7855
7890
|
return;
|
|
7856
|
-
//
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
return; // already after parent
|
|
7861
|
-
cursor = cursor.next;
|
|
7862
|
-
}
|
|
7863
|
-
// If we reach here, node is before parent — need to move
|
|
7891
|
+
// Positions are strictly increasing along the list, so this is an
|
|
7892
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
7893
|
+
if (node.position > parentNode.position)
|
|
7894
|
+
return;
|
|
7864
7895
|
// Remove node from current position
|
|
7865
7896
|
if (node.prev) {
|
|
7866
7897
|
node.prev.next = node.next;
|
|
@@ -7874,16 +7905,18 @@ class Root {
|
|
|
7874
7905
|
else {
|
|
7875
7906
|
changeSet.tail = node.prev;
|
|
7876
7907
|
}
|
|
7877
|
-
//
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7908
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
7909
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
7910
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
7911
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
7912
|
+
// a fresh max position, keeping the invariant append-only.
|
|
7913
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
7914
|
+
// subtree re-serializes parent-first behind it.)
|
|
7915
|
+
node.prev = changeSet.tail;
|
|
7916
|
+
node.next = undefined;
|
|
7917
|
+
changeSet.tail.next = node; // parentNode remains in the list — never empty here
|
|
7918
|
+
changeSet.tail = node;
|
|
7919
|
+
node.position = changeSet.nextPosition++;
|
|
7887
7920
|
}
|
|
7888
7921
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7889
7922
|
if (existingNode) {
|
|
@@ -7905,12 +7938,12 @@ class Root {
|
|
|
7905
7938
|
node.changeTree = changeTree;
|
|
7906
7939
|
node.next = undefined;
|
|
7907
7940
|
node.prev = undefined;
|
|
7908
|
-
node.position = 0;
|
|
7909
7941
|
}
|
|
7910
7942
|
else {
|
|
7911
7943
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7912
7944
|
}
|
|
7913
7945
|
if (!list.next) {
|
|
7946
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7914
7947
|
list.next = node;
|
|
7915
7948
|
list.tail = node;
|
|
7916
7949
|
}
|
|
@@ -7919,6 +7952,7 @@ class Root {
|
|
|
7919
7952
|
list.tail.next = node;
|
|
7920
7953
|
list.tail = node;
|
|
7921
7954
|
}
|
|
7955
|
+
node.position = list.nextPosition++;
|
|
7922
7956
|
return node;
|
|
7923
7957
|
}
|
|
7924
7958
|
/**
|
|
@@ -8006,17 +8040,7 @@ function _fullSyncWalk(ctx, changeTree) {
|
|
|
8006
8040
|
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
8007
8041
|
// nothing itself but we still recurse so descendants (possibly added to
|
|
8008
8042
|
// the view explicitly) are reachable.
|
|
8009
|
-
|
|
8010
|
-
if (ctx.hasView) {
|
|
8011
|
-
const view = ctx.view;
|
|
8012
|
-
if (!view.isChangeTreeVisible(changeTree)) {
|
|
8013
|
-
view.markInvisible(changeTree);
|
|
8014
|
-
visibleHere = false;
|
|
8015
|
-
}
|
|
8016
|
-
else {
|
|
8017
|
-
view.unmarkInvisible(changeTree);
|
|
8018
|
-
}
|
|
8019
|
-
}
|
|
8043
|
+
const visibleHere = !ctx.hasView || ctx.view.isChangeTreeVisible(changeTree);
|
|
8020
8044
|
if (visibleHere) {
|
|
8021
8045
|
const desc = changeTree.encDescriptor;
|
|
8022
8046
|
ctx.changeTree = changeTree;
|
|
@@ -8027,6 +8051,7 @@ function _fullSyncWalk(ctx, changeTree) {
|
|
|
8027
8051
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8028
8052
|
ctx.isSchema = desc.isSchema;
|
|
8029
8053
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8054
|
+
ctx.tags = desc.tags;
|
|
8030
8055
|
ctx.structSwitchEmitted = false;
|
|
8031
8056
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8032
8057
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8066,10 +8091,13 @@ function encodeChangeCb(ctx, fieldIndex, op) {
|
|
|
8066
8091
|
}
|
|
8067
8092
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8068
8093
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8069
|
-
// itself carries a @view tag.
|
|
8070
|
-
//
|
|
8094
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
8095
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
8096
|
+
// that arm needs a Schema with more than 32 fields.
|
|
8071
8097
|
const fieldFiltered = ctx.isSchema
|
|
8072
|
-
? (ctx.treeIsFiltered || (
|
|
8098
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8099
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8100
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8073
8101
|
: ctx.treeIsFiltered;
|
|
8074
8102
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8075
8103
|
return;
|
|
@@ -8124,7 +8152,7 @@ class Encoder {
|
|
|
8124
8152
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8125
8153
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8126
8154
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8127
|
-
filterBitmask: 0,
|
|
8155
|
+
filterBitmask: 0, tags: undefined,
|
|
8128
8156
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8129
8157
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8130
8158
|
};
|
|
@@ -8165,12 +8193,8 @@ class Encoder {
|
|
|
8165
8193
|
let current = queue;
|
|
8166
8194
|
while (current = current.next) {
|
|
8167
8195
|
const changeTree = current.changeTree;
|
|
8168
|
-
if (hasView) {
|
|
8169
|
-
|
|
8170
|
-
view.markInvisible(changeTree);
|
|
8171
|
-
continue;
|
|
8172
|
-
}
|
|
8173
|
-
view.unmarkInvisible(changeTree);
|
|
8196
|
+
if (hasView && !view.isChangeTreeVisible(changeTree)) {
|
|
8197
|
+
continue;
|
|
8174
8198
|
}
|
|
8175
8199
|
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
8176
8200
|
if (!recorder || !recorder.has()) {
|
|
@@ -8185,6 +8209,7 @@ class Encoder {
|
|
|
8185
8209
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8186
8210
|
ctx.isSchema = desc.isSchema;
|
|
8187
8211
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8212
|
+
ctx.tags = desc.tags;
|
|
8188
8213
|
ctx.structSwitchEmitted = false;
|
|
8189
8214
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8190
8215
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8436,7 +8461,7 @@ class Encoder {
|
|
|
8436
8461
|
// Emit each element's full state — forEachLive walks populated
|
|
8437
8462
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8438
8463
|
// Covers both static elements (dirty state was reset by
|
|
8439
|
-
// inheritedFlags'
|
|
8464
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8440
8465
|
// has dirty state but the main loop skipped them because
|
|
8441
8466
|
// they're filtered).
|
|
8442
8467
|
for (const element of emittedElements) {
|
|
@@ -8515,20 +8540,81 @@ class Encoder {
|
|
|
8515
8540
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8516
8541
|
// into `_stream.priority` when the stream was attached. Users
|
|
8517
8542
|
// can also override per-instance by assigning to the setter.
|
|
8543
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
8544
|
+
// wins over the declaration-scope one: it closes over the
|
|
8545
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
8546
|
+
const perView = st.priorityByView?.get(viewId);
|
|
8547
|
+
const usePerView = perView !== undefined;
|
|
8518
8548
|
const priority = st.priority;
|
|
8519
|
-
|
|
8520
|
-
//
|
|
8521
|
-
//
|
|
8549
|
+
const max = st.maxPerTick;
|
|
8550
|
+
// Select the `max` highest-priority candidates.
|
|
8551
|
+
//
|
|
8552
|
+
// A comparator-based sort invokes the callback twice per
|
|
8553
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
8554
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
8555
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
8556
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
8557
|
+
// the scratch by `max` rather than by the backlog.
|
|
8558
|
+
//
|
|
8559
|
+
// Ties keep the earlier position (both comparisons below are
|
|
8560
|
+
// strict), so equal-priority entries still drain in insertion
|
|
8561
|
+
// order.
|
|
8522
8562
|
const positions = [];
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8563
|
+
const stale = [];
|
|
8564
|
+
if (usePerView || priority !== undefined) {
|
|
8565
|
+
const bestPos = [];
|
|
8566
|
+
const bestScore = [];
|
|
8567
|
+
let filled = 0;
|
|
8568
|
+
for (const pos of pending) {
|
|
8569
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
8570
|
+
// through the same lookup regardless of $items layout.
|
|
8571
|
+
const element = s[$getByIndex](pos);
|
|
8572
|
+
if (element === undefined) {
|
|
8573
|
+
// Removed after being queued — drop it below without
|
|
8574
|
+
// spending budget on it.
|
|
8575
|
+
stale.push(pos);
|
|
8576
|
+
continue;
|
|
8577
|
+
}
|
|
8578
|
+
const score = usePerView
|
|
8579
|
+
? perView(element)
|
|
8580
|
+
: priority(view, element);
|
|
8581
|
+
// Window not yet full: always insert.
|
|
8582
|
+
if (filled < max) {
|
|
8583
|
+
let j = filled++;
|
|
8584
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8585
|
+
bestScore[j] = bestScore[j - 1];
|
|
8586
|
+
bestPos[j] = bestPos[j - 1];
|
|
8587
|
+
j--;
|
|
8588
|
+
}
|
|
8589
|
+
bestScore[j] = score;
|
|
8590
|
+
bestPos[j] = pos;
|
|
8591
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
8592
|
+
}
|
|
8593
|
+
else if (score > bestScore[max - 1]) {
|
|
8594
|
+
let j = max - 1;
|
|
8595
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8596
|
+
bestScore[j] = bestScore[j - 1];
|
|
8597
|
+
bestPos[j] = bestPos[j - 1];
|
|
8598
|
+
j--;
|
|
8599
|
+
}
|
|
8600
|
+
bestScore[j] = score;
|
|
8601
|
+
bestPos[j] = pos;
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
for (let i = 0; i < filled; i++)
|
|
8605
|
+
positions.push(bestPos[i]);
|
|
8529
8606
|
}
|
|
8530
|
-
|
|
8531
|
-
|
|
8607
|
+
else {
|
|
8608
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
8609
|
+
for (const pos of pending) {
|
|
8610
|
+
if (positions.length >= max)
|
|
8611
|
+
break;
|
|
8612
|
+
positions.push(pos);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
for (const pos of stale)
|
|
8616
|
+
pending.delete(pos);
|
|
8617
|
+
const count = positions.length;
|
|
8532
8618
|
let sent = st.sentByView.get(viewId);
|
|
8533
8619
|
if (sent === undefined) {
|
|
8534
8620
|
sent = new Set();
|
|
@@ -9862,9 +9948,6 @@ function _clearViewBitFromAllTrees(root, slot, bit) {
|
|
|
9862
9948
|
const v = tree.visibleViews;
|
|
9863
9949
|
if (v !== undefined && slot < v.length)
|
|
9864
9950
|
v[slot] &= clearMask;
|
|
9865
|
-
const i = tree.invisibleViews;
|
|
9866
|
-
if (i !== undefined && slot < i.length)
|
|
9867
|
-
i[slot] &= clearMask;
|
|
9868
9951
|
const s = tree.subscribedViews;
|
|
9869
9952
|
if (s !== undefined && slot < s.length)
|
|
9870
9953
|
s[slot] &= clearMask;
|
|
@@ -9886,6 +9969,31 @@ const _disposeRegistry = new FinalizationRegistry(({ root, id, slot, bit }) => {
|
|
|
9886
9969
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9887
9970
|
root.releaseViewId(id);
|
|
9888
9971
|
});
|
|
9972
|
+
/**
|
|
9973
|
+
* Compact description of a rejected argument, for warning messages.
|
|
9974
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
9975
|
+
* populated collection inspects into dozens of lines of encoder
|
|
9976
|
+
* internals and buries the message that matters.
|
|
9977
|
+
*/
|
|
9978
|
+
function describeArg(value) {
|
|
9979
|
+
if (value === undefined) {
|
|
9980
|
+
return "undefined";
|
|
9981
|
+
}
|
|
9982
|
+
if (value === null) {
|
|
9983
|
+
return "null";
|
|
9984
|
+
}
|
|
9985
|
+
const type = typeof value;
|
|
9986
|
+
if (type === "string") {
|
|
9987
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
9988
|
+
}
|
|
9989
|
+
if (type !== "object" && type !== "function") {
|
|
9990
|
+
return `${type} ${String(value)}`;
|
|
9991
|
+
}
|
|
9992
|
+
if (Array.isArray(value)) {
|
|
9993
|
+
return `Array(${value.length})`;
|
|
9994
|
+
}
|
|
9995
|
+
return value.constructor?.name ?? "Object";
|
|
9996
|
+
}
|
|
9889
9997
|
class StateView {
|
|
9890
9998
|
iterable;
|
|
9891
9999
|
/**
|
|
@@ -10016,32 +10124,6 @@ class StateView {
|
|
|
10016
10124
|
if (slot < arr.length)
|
|
10017
10125
|
arr[slot] &= ~this._bit;
|
|
10018
10126
|
}
|
|
10019
|
-
/** True iff this view has previously marked `tree` as invisible. */
|
|
10020
|
-
isInvisible(tree) {
|
|
10021
|
-
const arr = tree.invisibleViews;
|
|
10022
|
-
const slot = this._slot;
|
|
10023
|
-
return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
|
|
10024
|
-
}
|
|
10025
|
-
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
10026
|
-
markInvisible(tree) {
|
|
10027
|
-
const slot = this._slot;
|
|
10028
|
-
let arr = tree.invisibleViews;
|
|
10029
|
-
if (arr === undefined) {
|
|
10030
|
-
arr = tree.invisibleViews = [];
|
|
10031
|
-
}
|
|
10032
|
-
while (arr.length <= slot)
|
|
10033
|
-
arr.push(0);
|
|
10034
|
-
arr[slot] |= this._bit;
|
|
10035
|
-
}
|
|
10036
|
-
/** Clear invisible bit. */
|
|
10037
|
-
unmarkInvisible(tree) {
|
|
10038
|
-
const arr = tree.invisibleViews;
|
|
10039
|
-
if (arr === undefined)
|
|
10040
|
-
return;
|
|
10041
|
-
const slot = this._slot;
|
|
10042
|
-
if (slot < arr.length)
|
|
10043
|
-
arr[slot] &= ~this._bit;
|
|
10044
|
-
}
|
|
10045
10127
|
// ──────────────────────────────────────────────────────────────────
|
|
10046
10128
|
// Per-tag, per-view bitmap. Replaces the legacy
|
|
10047
10129
|
// `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
|
|
@@ -10136,12 +10218,12 @@ class StateView {
|
|
|
10136
10218
|
}
|
|
10137
10219
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10138
10220
|
const changeTree = obj?.[$changes];
|
|
10139
|
-
const parentChangeTree = changeTree.parent;
|
|
10140
10221
|
if (!changeTree) {
|
|
10141
|
-
console.warn(
|
|
10222
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10142
10223
|
return false;
|
|
10143
10224
|
}
|
|
10144
|
-
|
|
10225
|
+
const parentChangeTree = changeTree.parent;
|
|
10226
|
+
if (!parentChangeTree &&
|
|
10145
10227
|
obj[$refId] !== 0 // allow root object
|
|
10146
10228
|
) {
|
|
10147
10229
|
/**
|
|
@@ -10185,11 +10267,18 @@ class StateView {
|
|
|
10185
10267
|
// below use `metadata?.[...]` null-safe access. Only Schema
|
|
10186
10268
|
// subclasses yield a real Metadata object.
|
|
10187
10269
|
const metadata = obj.constructor[Symbol.metadata];
|
|
10188
|
-
|
|
10189
|
-
//
|
|
10190
|
-
|
|
10270
|
+
// Add to iterable list (only the explicitly added items), deduping
|
|
10271
|
+
// re-adds of an already-visible instance. isVisible must be read
|
|
10272
|
+
// BEFORE markVisible; indexOf runs only on the re-add path.
|
|
10273
|
+
// NOTE: dedup applies to `items` only — a re-add still re-queues the
|
|
10274
|
+
// full snapshot on purpose (shared-view bootstrap re-add: a
|
|
10275
|
+
// late-attached client may not have consumed earlier drains).
|
|
10276
|
+
// Callers wanting cheap idempotence can guard with `view.has(obj)`.
|
|
10277
|
+
if (this.iterable && checkIncludeParent
|
|
10278
|
+
&& (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
|
|
10191
10279
|
this.items.push(obj);
|
|
10192
10280
|
}
|
|
10281
|
+
this.markVisible(changeTree);
|
|
10193
10282
|
// add parent ChangeTree's
|
|
10194
10283
|
// - if it was invisible to this view
|
|
10195
10284
|
// - if it were previously filtered out
|
|
@@ -10272,18 +10361,20 @@ class StateView {
|
|
|
10272
10361
|
}
|
|
10273
10362
|
else if (!changeTree.isNew || isChildAdded) {
|
|
10274
10363
|
// new structures will be added as part of .encode() call, no need to force it to .encodeView()
|
|
10275
|
-
const isInvisible = this.isInvisible(changeTree);
|
|
10276
10364
|
// Full-sync snapshot: walk the live ref structurally instead of
|
|
10277
10365
|
// iterating a cumulative recorder bucket. Every populated index
|
|
10278
10366
|
// is emitted as ADD (matching the op-coercion previously done
|
|
10279
10367
|
// at encode time). Per-field tags come from the descriptor's
|
|
10280
10368
|
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
10281
10369
|
// object hop.
|
|
10370
|
+
//
|
|
10371
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
10372
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
10373
|
+
// so anything added leaks straight to the wire.
|
|
10282
10374
|
const tags = changeTree.encDescriptor.tags;
|
|
10283
10375
|
changeTree.forEachLive((index) => {
|
|
10284
10376
|
const tagAtIndex = tags[index];
|
|
10285
|
-
if (
|
|
10286
|
-
tagAtIndex === undefined || // "all change" with no tag
|
|
10377
|
+
if (tagAtIndex === undefined || // "all change" with no tag
|
|
10287
10378
|
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
10288
10379
|
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
10289
10380
|
) {
|
|
@@ -10413,9 +10504,9 @@ class StateView {
|
|
|
10413
10504
|
}
|
|
10414
10505
|
}
|
|
10415
10506
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10416
|
-
const changeTree = obj[$changes];
|
|
10507
|
+
const changeTree = obj?.[$changes];
|
|
10417
10508
|
if (!changeTree) {
|
|
10418
|
-
console.warn(
|
|
10509
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10419
10510
|
return this;
|
|
10420
10511
|
}
|
|
10421
10512
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10435,7 +10526,7 @@ class StateView {
|
|
|
10435
10526
|
}
|
|
10436
10527
|
// ── Streamable-collection unsubscribe (the stream itself) ─────
|
|
10437
10528
|
// Flush DELETE for every sent position and drop pending. After
|
|
10438
|
-
// this, the stream is
|
|
10529
|
+
// this, the stream is no longer visible to this view — any future
|
|
10439
10530
|
// `stream.add()` would still seed broadcast pending (if no views)
|
|
10440
10531
|
// but would NOT re-seed per-view pending (user must re-subscribe).
|
|
10441
10532
|
if (changeTree.isStreamCollection) {
|
|
@@ -10553,32 +10644,47 @@ class StateView {
|
|
|
10553
10644
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10554
10645
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10555
10646
|
}
|
|
10556
|
-
|
|
10557
|
-
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
10558
|
-
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
10559
|
-
* future content changes — every subsequent push / set / add to the
|
|
10560
|
-
* collection automatically flows to this view, and every removal
|
|
10561
|
-
* queues a DELETE op. Works on every collection type:
|
|
10562
|
-
*
|
|
10563
|
-
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
10564
|
-
* new children are force-shipped immediately (equivalent to
|
|
10565
|
-
* `view.add(child)` per item).
|
|
10566
|
-
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
10567
|
-
* enqueued into `_pendingByView` so the priority pass drains them
|
|
10568
|
-
* respecting `maxPerTick`.
|
|
10569
|
-
*
|
|
10570
|
-
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
10571
|
-
* collection is a no-op.
|
|
10572
|
-
*/
|
|
10573
|
-
subscribe(collection) {
|
|
10647
|
+
subscribe(collection, priority) {
|
|
10574
10648
|
const tree = collection?.[$changes];
|
|
10575
10649
|
if (!tree) {
|
|
10576
|
-
console.warn(
|
|
10650
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10577
10651
|
return this;
|
|
10578
10652
|
}
|
|
10579
10653
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10580
10654
|
this._bindRoot(tree.root);
|
|
10581
10655
|
}
|
|
10656
|
+
if (priority !== undefined) {
|
|
10657
|
+
if (!tree.isStreamCollection) {
|
|
10658
|
+
// Name the field rather than dumping the collection — a
|
|
10659
|
+
// populated MapSchema inspects into dozens of lines of
|
|
10660
|
+
// internals and buries the message.
|
|
10661
|
+
const kind = collection?.constructor?.name ?? "collection";
|
|
10662
|
+
const parent = tree.parent;
|
|
10663
|
+
if (parent === undefined) {
|
|
10664
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
10665
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
10666
|
+
`Subscribe after assigning it to the state.`);
|
|
10667
|
+
}
|
|
10668
|
+
else {
|
|
10669
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
10670
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
10671
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
10672
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
10673
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`);
|
|
10674
|
+
}
|
|
10675
|
+
}
|
|
10676
|
+
else {
|
|
10677
|
+
// Set before the idempotency return below, so re-subscribing
|
|
10678
|
+
// is the documented way to retarget this view's ordering.
|
|
10679
|
+
const st = ensureStreamState(collection);
|
|
10680
|
+
if (priority === null) {
|
|
10681
|
+
st.priorityByView?.delete(this.id);
|
|
10682
|
+
}
|
|
10683
|
+
else {
|
|
10684
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
}
|
|
10582
10688
|
if (this.isSubscribed(tree))
|
|
10583
10689
|
return this;
|
|
10584
10690
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10624,7 +10730,7 @@ class StateView {
|
|
|
10624
10730
|
unsubscribe(collection) {
|
|
10625
10731
|
const tree = collection?.[$changes];
|
|
10626
10732
|
if (!tree) {
|
|
10627
|
-
console.warn(
|
|
10733
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10628
10734
|
return this;
|
|
10629
10735
|
}
|
|
10630
10736
|
if (!this.isSubscribed(tree))
|
|
@@ -10753,15 +10859,15 @@ exports.encodeKeyValueOperation = encodeKeyValueOperation;
|
|
|
10753
10859
|
exports.encodeMapEntry = encodeMapEntry;
|
|
10754
10860
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
10755
10861
|
exports.entity = entity;
|
|
10862
|
+
exports.fullStateOnly = fullStateOnly;
|
|
10756
10863
|
exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
|
|
10757
10864
|
exports.getEncodeDescriptor = getEncodeDescriptor;
|
|
10758
10865
|
exports.getRawChangesCallback = getRawChangesCallback;
|
|
10759
10866
|
exports.isBuilder = isBuilder;
|
|
10760
|
-
exports.
|
|
10867
|
+
exports.patchOnly = patchOnly;
|
|
10761
10868
|
exports.registerType = registerType;
|
|
10762
10869
|
exports.schema = schema;
|
|
10763
10870
|
exports.t = t;
|
|
10764
|
-
exports.transient = transient;
|
|
10765
10871
|
exports.type = type;
|
|
10766
10872
|
exports.unreliable = unreliable;
|
|
10767
10873
|
exports.view = view;
|