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