@colyseus/schema 5.0.11 → 5.0.13
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 +25 -9
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -2
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/changeTree/parentChain.d.ts +9 -0
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +449 -233
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +449 -233
- package/build/index.mjs +448 -232
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/ArraySchema.d.ts +17 -0
- 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 +46 -26
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +97 -21
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +102 -12
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/changeTree/parentChain.ts +29 -0
- 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/ArraySchema.ts +40 -1
- 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,
|
|
@@ -1916,6 +1912,34 @@ function addParent(tree, parent, index) {
|
|
|
1916
1912
|
tree._parentIndex = index;
|
|
1917
1913
|
}
|
|
1918
1914
|
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Move `parent`'s existing chain entry to `index`, skipping the attachment
|
|
1917
|
+
* work `addParent` does. `parent` must already be a parent of `tree`.
|
|
1918
|
+
*
|
|
1919
|
+
* Called by collections whose wire slots shift (ArraySchema): StateView
|
|
1920
|
+
* addresses per-view ADD/DELETE by that index, so it has to follow the
|
|
1921
|
+
* element it names.
|
|
1922
|
+
*/
|
|
1923
|
+
function setParentIndex(tree, parent, index) {
|
|
1924
|
+
if (tree.extraParents === undefined) {
|
|
1925
|
+
tree._parentIndex = index; // sole parent, so it is `parent`
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
// Shared instance — move only the entry `parent` owns. Matching goes
|
|
1929
|
+
// through `$changes` because ArraySchema arrives proxied (see removeParent
|
|
1930
|
+
// below), and `extraParents` only ever fills by demoting `parentRef`, so
|
|
1931
|
+
// the inline parent is set here.
|
|
1932
|
+
if (tree.parentRef[$changes] === parent[$changes]) {
|
|
1933
|
+
tree._parentIndex = index;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
1937
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
1938
|
+
entry.index = index;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1919
1943
|
/**
|
|
1920
1944
|
* Remove a parent from the chain.
|
|
1921
1945
|
* @returns true if parent was found and removed (Root.remove relies on this).
|
|
@@ -1993,13 +2017,13 @@ function getAllParents(tree) {
|
|
|
1993
2017
|
}
|
|
1994
2018
|
|
|
1995
2019
|
/**
|
|
1996
|
-
* Walk all currently-populated non-
|
|
2020
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
1997
2021
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
1998
2022
|
* StateView.add to derive full-sync output from the live structure.
|
|
1999
2023
|
*
|
|
2000
|
-
*
|
|
2024
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
2001
2025
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
2002
|
-
* field is @
|
|
2026
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
2003
2027
|
*/
|
|
2004
2028
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
2005
2029
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
@@ -2013,10 +2037,10 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2013
2037
|
// types. See `ChangeTree.refTarget` doc.
|
|
2014
2038
|
const ref = tree.refTarget;
|
|
2015
2039
|
if (ref[$childType] !== undefined) {
|
|
2016
|
-
// Collection inheriting @
|
|
2040
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
2017
2041
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
2018
2042
|
// absent from full-sync output is never pruned client-side.
|
|
2019
|
-
if (tree.
|
|
2043
|
+
if (tree.isPatchOnly)
|
|
2020
2044
|
return;
|
|
2021
2045
|
// Collection types: dispatch by shape.
|
|
2022
2046
|
if (Array.isArray(ref.items)) {
|
|
@@ -2045,7 +2069,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2045
2069
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
2046
2070
|
// the setter records a DELETE when a field is set to null or
|
|
2047
2071
|
// undefined, so it should not appear in full-sync output.
|
|
2048
|
-
// (@
|
|
2072
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
2049
2073
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
2050
2074
|
//
|
|
2051
2075
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -2055,13 +2079,13 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2055
2079
|
if (!metadata)
|
|
2056
2080
|
return;
|
|
2057
2081
|
const numFields = (metadata[$numFields] ?? -1);
|
|
2058
|
-
const
|
|
2082
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
2059
2083
|
const names = tree.encDescriptor.names;
|
|
2060
2084
|
for (let i = 0; i <= numFields; i++) {
|
|
2061
2085
|
const name = names[i];
|
|
2062
2086
|
if (name === undefined)
|
|
2063
2087
|
continue;
|
|
2064
|
-
if (
|
|
2088
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i))
|
|
2065
2089
|
continue;
|
|
2066
2090
|
const value = ref[name];
|
|
2067
2091
|
if (value !== undefined && value !== null)
|
|
@@ -2071,7 +2095,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2071
2095
|
}
|
|
2072
2096
|
|
|
2073
2097
|
/**
|
|
2074
|
-
* Filter / unreliable /
|
|
2098
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
2075
2099
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
2076
2100
|
* the parent field's annotation + the parent tree's own state.
|
|
2077
2101
|
*/
|
|
@@ -2084,7 +2108,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2084
2108
|
checkInheritedFlags(tree, parent, parentIndex);
|
|
2085
2109
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
2086
2110
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
2087
|
-
if (tree.
|
|
2111
|
+
if (tree.isFullStateOnly)
|
|
2088
2112
|
return;
|
|
2089
2113
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
2090
2114
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -2110,7 +2134,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2110
2134
|
}
|
|
2111
2135
|
}
|
|
2112
2136
|
/**
|
|
2113
|
-
* Inherit filter / unreliable /
|
|
2137
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
2114
2138
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
2115
2139
|
* etc.) inherit these from the Schema field that holds them.
|
|
2116
2140
|
*
|
|
@@ -2149,14 +2173,14 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2149
2173
|
parentIndex = parentChangeTree.parentIndex;
|
|
2150
2174
|
}
|
|
2151
2175
|
const parentMetadata = parent?.constructor?.[Symbol.metadata];
|
|
2152
|
-
// Flag inheritance — pack the
|
|
2176
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
2153
2177
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
2154
2178
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
2155
2179
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
2156
2180
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
2157
2181
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
2158
|
-
const fieldBits = (parentMetadata?.[$
|
|
2159
|
-
| (parentMetadata?.[$
|
|
2182
|
+
const fieldBits = (parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
2183
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
2160
2184
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
2161
2185
|
const beforeFlags = tree.flags;
|
|
2162
2186
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -2166,7 +2190,7 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2166
2190
|
// `new Config().assign({...})` populates the recorder before the
|
|
2167
2191
|
// Config instance is attached). Static trees ship state via structural
|
|
2168
2192
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
2169
|
-
if (gainedBits &
|
|
2193
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
2170
2194
|
tree.reset();
|
|
2171
2195
|
tree.unreliableRecorder?.reset();
|
|
2172
2196
|
}
|
|
@@ -2471,7 +2495,7 @@ function _setParentChildCb(ctx, child, index) {
|
|
|
2471
2495
|
*
|
|
2472
2496
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
2473
2497
|
* - liveIteration.ts forEachLive
|
|
2474
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
2498
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
2475
2499
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
2476
2500
|
*
|
|
2477
2501
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -2491,12 +2515,12 @@ function readInlineOpByte(low, high, index) {
|
|
|
2491
2515
|
const _invokeNoCtx = (cb, index, op) => cb(index, op);
|
|
2492
2516
|
// Linked list helper functions
|
|
2493
2517
|
function createChangeTreeList() {
|
|
2494
|
-
return { next: undefined, tail: undefined };
|
|
2518
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
2495
2519
|
}
|
|
2496
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
2520
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
2497
2521
|
// field's annotation — inherited at setParent/setRoot time.
|
|
2498
2522
|
const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
2499
|
-
const IS_UNRELIABLE = 8,
|
|
2523
|
+
const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
2500
2524
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
2501
2525
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
2502
2526
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -2523,7 +2547,7 @@ const NEEDS_RESTAGE = 128;
|
|
|
2523
2547
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
2524
2548
|
* field mutations only) is designed later.
|
|
2525
2549
|
*/
|
|
2526
|
-
const INHERITABLE_FLAGS =
|
|
2550
|
+
const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
2527
2551
|
class ChangeTree {
|
|
2528
2552
|
ref;
|
|
2529
2553
|
/**
|
|
@@ -2542,8 +2566,8 @@ class ChangeTree {
|
|
|
2542
2566
|
refTarget;
|
|
2543
2567
|
metadata;
|
|
2544
2568
|
/**
|
|
2545
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
2546
|
-
*
|
|
2569
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
2570
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
2547
2571
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
2548
2572
|
* EncodeDescriptor.ts.
|
|
2549
2573
|
*/
|
|
@@ -2614,10 +2638,10 @@ class ChangeTree {
|
|
|
2614
2638
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2615
2639
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2616
2640
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2617
|
-
get
|
|
2618
|
-
set
|
|
2619
|
-
get
|
|
2620
|
-
set
|
|
2641
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
2642
|
+
set isPatchOnly(v) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
2643
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
2644
|
+
set isFullStateOnly(v) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
2621
2645
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2622
2646
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2623
2647
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2626,7 +2650,7 @@ class ChangeTree {
|
|
|
2626
2650
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2627
2651
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2628
2652
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2629
|
-
// pattern as `
|
|
2653
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2630
2654
|
get hasFilteredFields() {
|
|
2631
2655
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2632
2656
|
}
|
|
@@ -2650,7 +2674,7 @@ class ChangeTree {
|
|
|
2650
2674
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2651
2675
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2652
2676
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2653
|
-
// (
|
|
2677
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2654
2678
|
const desc = this.encDescriptor;
|
|
2655
2679
|
if (!desc.hasAnyUnreliable)
|
|
2656
2680
|
return false;
|
|
@@ -2660,15 +2684,15 @@ class ChangeTree {
|
|
|
2660
2684
|
}
|
|
2661
2685
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2662
2686
|
// by the tracker (the value still lives on the instance).
|
|
2663
|
-
|
|
2664
|
-
if (this.
|
|
2687
|
+
isFieldFullStateOnly(index) {
|
|
2688
|
+
if (this.isFullStateOnly)
|
|
2665
2689
|
return true;
|
|
2666
2690
|
const desc = this.encDescriptor;
|
|
2667
|
-
if (!desc.
|
|
2691
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2668
2692
|
return false;
|
|
2669
2693
|
if (index < 32)
|
|
2670
|
-
return (desc.
|
|
2671
|
-
return Metadata.
|
|
2694
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2695
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2672
2696
|
}
|
|
2673
2697
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2674
2698
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2927,7 +2951,7 @@ class ChangeTree {
|
|
|
2927
2951
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2928
2952
|
this.unreliableRecorder?.reset();
|
|
2929
2953
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2930
|
-
// (FILTERED/
|
|
2954
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2931
2955
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2932
2956
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2933
2957
|
this._fullSyncGen = 0;
|
|
@@ -2960,7 +2984,7 @@ class ChangeTree {
|
|
|
2960
2984
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2961
2985
|
const src = this.collDirty;
|
|
2962
2986
|
const dst = new Map();
|
|
2963
|
-
const track = !this.paused && !this.
|
|
2987
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2964
2988
|
if (track) {
|
|
2965
2989
|
for (let i = 0; i < count; i++)
|
|
2966
2990
|
dst.set(i, OPERATION.ADD);
|
|
@@ -2984,7 +3008,7 @@ class ChangeTree {
|
|
|
2984
3008
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2985
3009
|
}
|
|
2986
3010
|
operation(op) {
|
|
2987
|
-
if (this.paused || this.
|
|
3011
|
+
if (this.paused || this.isFullStateOnly)
|
|
2988
3012
|
return;
|
|
2989
3013
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2990
3014
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3014,11 +3038,23 @@ class ChangeTree {
|
|
|
3014
3038
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3015
3039
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3016
3040
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
3041
|
+
*
|
|
3042
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
3043
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
3044
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
3045
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
3046
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
3047
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
3048
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
3049
|
+
* which has always seeded these fields for late joiners.
|
|
3050
|
+
*
|
|
3051
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
3052
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
3017
3053
|
*/
|
|
3018
3054
|
_routeAndRecord(index, op, raw) {
|
|
3019
|
-
if (this.paused || this.
|
|
3055
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3020
3056
|
return;
|
|
3021
|
-
if (this.isFieldUnreliable(index)) {
|
|
3057
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3022
3058
|
const r = this.ensureUnreliableRecorder();
|
|
3023
3059
|
if (raw)
|
|
3024
3060
|
r.recordRaw(index, op);
|
|
@@ -3085,9 +3121,11 @@ class ChangeTree {
|
|
|
3085
3121
|
}
|
|
3086
3122
|
return;
|
|
3087
3123
|
}
|
|
3088
|
-
if (this.paused || this.
|
|
3124
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3089
3125
|
return this.getValue(index);
|
|
3090
|
-
|
|
3126
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
3127
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
3128
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
3091
3129
|
if (unreliable)
|
|
3092
3130
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? OPERATION.DELETE);
|
|
3093
3131
|
else
|
|
@@ -3146,6 +3184,8 @@ class ChangeTree {
|
|
|
3146
3184
|
get parent() { return this.parentRef; }
|
|
3147
3185
|
get parentIndex() { return this._parentIndex; }
|
|
3148
3186
|
addParent(parent, index) { addParent(this, parent, index); }
|
|
3187
|
+
/** Re-point an existing parent's cached index after the parent reindexed. */
|
|
3188
|
+
setParentIndex(parent, index) { setParentIndex(this, parent, index); }
|
|
3149
3189
|
/** @returns true if parent was found and removed */
|
|
3150
3190
|
removeParent(parent = this.parent) { return removeParent(this, parent); }
|
|
3151
3191
|
findParent(predicate) {
|
|
@@ -3190,6 +3230,7 @@ class UntrackedChangeTree {
|
|
|
3190
3230
|
operation() { }
|
|
3191
3231
|
setParent() { }
|
|
3192
3232
|
addParent() { }
|
|
3233
|
+
setParentIndex() { }
|
|
3193
3234
|
removeParent() { return false; }
|
|
3194
3235
|
getChange() { return 0; }
|
|
3195
3236
|
discard() { }
|
|
@@ -3293,7 +3334,9 @@ function encodeValue(encoder, bytes, type, value, operation, it, encoderFn) {
|
|
|
3293
3334
|
* @private
|
|
3294
3335
|
*/
|
|
3295
3336
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3296
|
-
// "compress" field index + operation
|
|
3337
|
+
// "compress" field index + operation. Can't collide with
|
|
3338
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
3339
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
3297
3340
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3298
3341
|
// Do not encode value for DELETE operations
|
|
3299
3342
|
if (operation === OPERATION.DELETE) {
|
|
@@ -3489,8 +3532,8 @@ function resyncTouchEntry(decoder, ref, operation, identity, previousValue, valu
|
|
|
3489
3532
|
/**
|
|
3490
3533
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3491
3534
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3492
|
-
* of full-sync" (@
|
|
3493
|
-
* live data. Reflected clients have no @
|
|
3535
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
3536
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
3494
3537
|
* presence is the only reliable signal.
|
|
3495
3538
|
*/
|
|
3496
3539
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3504,7 +3547,7 @@ function resyncMarkPresent(decoder, refId) {
|
|
|
3504
3547
|
* entry the snapshot did not visit.
|
|
3505
3548
|
*
|
|
3506
3549
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3507
|
-
* `@
|
|
3550
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3508
3551
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3509
3552
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3510
3553
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3529,11 +3572,11 @@ function sweepSchema(decoder, ref, seen, allChanges) {
|
|
|
3529
3572
|
if (refIndexes === undefined) {
|
|
3530
3573
|
return;
|
|
3531
3574
|
}
|
|
3532
|
-
const
|
|
3575
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3533
3576
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3534
3577
|
const fieldIndex = refIndexes[i];
|
|
3535
|
-
// @
|
|
3536
|
-
if (
|
|
3578
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3579
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3537
3580
|
continue;
|
|
3538
3581
|
}
|
|
3539
3582
|
const field = metadata[fieldIndex];
|
|
@@ -3558,7 +3601,7 @@ function sweepCollection(decoder, coll, seen, allChanges) {
|
|
|
3558
3601
|
seen.add(refId);
|
|
3559
3602
|
// `undefined` = the collection never appeared in the payload at all
|
|
3560
3603
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3561
|
-
// (@
|
|
3604
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3562
3605
|
// means "present with zero entries" → prune everything.
|
|
3563
3606
|
const visited = decoder.resyncVisited.get(refId);
|
|
3564
3607
|
if (visited === undefined) {
|
|
@@ -4243,6 +4286,40 @@ class ArraySchema {
|
|
|
4243
4286
|
// beyond the live range: appends land after the staged tmpItems tail
|
|
4244
4287
|
return tmpItems.length + (index - live);
|
|
4245
4288
|
}
|
|
4289
|
+
/**
|
|
4290
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
4291
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
4292
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
4293
|
+
* at whichever element inherited the slot (issue #231).
|
|
4294
|
+
*
|
|
4295
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
4296
|
+
* the only reader and reaches the index only through a filtered array
|
|
4297
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
4298
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
4299
|
+
* its children every tick.
|
|
4300
|
+
*
|
|
4301
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
4302
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
4303
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
4304
|
+
*/
|
|
4305
|
+
$reindexChildren(from, staged) {
|
|
4306
|
+
if (!this[$changes].hasFilteredFields) {
|
|
4307
|
+
return;
|
|
4308
|
+
} // nothing will read the cache
|
|
4309
|
+
if (typeof this[$childType] === "string") {
|
|
4310
|
+
return;
|
|
4311
|
+
} // primitives have no child tree
|
|
4312
|
+
const tmpItems = this.tmpItems;
|
|
4313
|
+
const length = tmpItems.length;
|
|
4314
|
+
if (staged !== undefined) {
|
|
4315
|
+
while (from < length && tmpItems[from] === staged[from]) {
|
|
4316
|
+
from++;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
for (let i = from; i < length; i++) {
|
|
4320
|
+
tmpItems[i]?.[$changes]?.setParentIndex(this, i);
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4246
4323
|
// encoding only. Returns the wire index the change was recorded at
|
|
4247
4324
|
// (undefined when nothing was recorded).
|
|
4248
4325
|
$changeAt(index, value) {
|
|
@@ -4354,6 +4431,7 @@ class ArraySchema {
|
|
|
4354
4431
|
self[$changes].operation(OPERATION.REVERSE);
|
|
4355
4432
|
self.items.reverse();
|
|
4356
4433
|
self.tmpItems.reverse();
|
|
4434
|
+
self.$reindexChildren(0);
|
|
4357
4435
|
return this;
|
|
4358
4436
|
}
|
|
4359
4437
|
/**
|
|
@@ -4405,6 +4483,7 @@ class ArraySchema {
|
|
|
4405
4483
|
// wouldn't OPERATION.MOVE make more sense here?
|
|
4406
4484
|
sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
|
|
4407
4485
|
self.tmpItems.sort(compareFn);
|
|
4486
|
+
self.$reindexChildren(0);
|
|
4408
4487
|
self.isMovingItems = false;
|
|
4409
4488
|
return this;
|
|
4410
4489
|
}
|
|
@@ -4486,6 +4565,7 @@ class ArraySchema {
|
|
|
4486
4565
|
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
4487
4566
|
}
|
|
4488
4567
|
self.tmpItems.unshift(...items);
|
|
4568
|
+
self.$reindexChildren(items.length); // survivors only — the loop above placed the new items
|
|
4489
4569
|
return self.items.unshift(...items);
|
|
4490
4570
|
}
|
|
4491
4571
|
/**
|
|
@@ -4754,8 +4834,14 @@ class ArraySchema {
|
|
|
4754
4834
|
}
|
|
4755
4835
|
[$onEncodeEnd]() {
|
|
4756
4836
|
const self = this[$proxyTarget] ?? this;
|
|
4837
|
+
const staged = self.tmpItems;
|
|
4757
4838
|
self.tmpItems = self.items.slice();
|
|
4758
|
-
self.deletedIndexes.length
|
|
4839
|
+
if (self.deletedIndexes.length > 0) {
|
|
4840
|
+
// compaction just closed the staged holes — everything above the
|
|
4841
|
+
// lowest one slid down a slot
|
|
4842
|
+
self.$reindexChildren(0, staged);
|
|
4843
|
+
self.deletedIndexes.length = 0;
|
|
4844
|
+
}
|
|
4759
4845
|
}
|
|
4760
4846
|
[$onDecodeEnd]() {
|
|
4761
4847
|
const self = this[$proxyTarget] ?? this;
|
|
@@ -5877,7 +5963,7 @@ registerType("set", { constructor: SetSchema });
|
|
|
5877
5963
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5878
5964
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5879
5965
|
* propagate through the normal reliable channel without consuming the
|
|
5880
|
-
* per-tick budget. Chain `.
|
|
5966
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5881
5967
|
* post-add mutation tracking entirely.
|
|
5882
5968
|
*/
|
|
5883
5969
|
class StreamSchema {
|
|
@@ -6152,12 +6238,11 @@ class FieldBuilder {
|
|
|
6152
6238
|
_default = undefined;
|
|
6153
6239
|
_hasDefault = false;
|
|
6154
6240
|
_view = undefined;
|
|
6155
|
-
_owned = false;
|
|
6156
6241
|
_unreliable = false;
|
|
6157
|
-
|
|
6242
|
+
_patchOnly = false;
|
|
6158
6243
|
_deprecated = false;
|
|
6159
6244
|
_deprecatedThrows = true;
|
|
6160
|
-
|
|
6245
|
+
_fullStateOnly = false;
|
|
6161
6246
|
_stream = false;
|
|
6162
6247
|
_optional = false;
|
|
6163
6248
|
_noSync = false;
|
|
@@ -6192,38 +6277,43 @@ class FieldBuilder {
|
|
|
6192
6277
|
this._view = tag ?? -1;
|
|
6193
6278
|
return this;
|
|
6194
6279
|
}
|
|
6195
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6196
|
-
owned() {
|
|
6197
|
-
this._owned = true;
|
|
6198
|
-
return this;
|
|
6199
|
-
}
|
|
6200
6280
|
/**
|
|
6201
6281
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6202
6282
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6203
|
-
* tagged with `.
|
|
6283
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
6284
|
+
*
|
|
6285
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
6286
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
6287
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
6288
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
6289
|
+
* the field is never written again.
|
|
6204
6290
|
*/
|
|
6205
6291
|
unreliable() {
|
|
6206
6292
|
this._unreliable = true;
|
|
6207
6293
|
return this;
|
|
6208
6294
|
}
|
|
6209
6295
|
/**
|
|
6210
|
-
*
|
|
6211
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6212
|
-
* only after its next mutation is emitted on a
|
|
6213
|
-
* to `.unreliable()`.
|
|
6296
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
6297
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6298
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
6299
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
6214
6300
|
*/
|
|
6215
|
-
|
|
6216
|
-
this.
|
|
6301
|
+
patchOnly() {
|
|
6302
|
+
this._patchOnly = true;
|
|
6217
6303
|
return this;
|
|
6218
6304
|
}
|
|
6219
6305
|
/**
|
|
6220
|
-
*
|
|
6221
|
-
*
|
|
6222
|
-
*
|
|
6223
|
-
*
|
|
6306
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
6307
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
6308
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
6309
|
+
* The mirror of `.patchOnly()`.
|
|
6310
|
+
*
|
|
6311
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
6312
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
6313
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
6224
6314
|
*/
|
|
6225
|
-
|
|
6226
|
-
this.
|
|
6315
|
+
fullStateOnly() {
|
|
6316
|
+
this._fullStateOnly = true;
|
|
6227
6317
|
return this;
|
|
6228
6318
|
}
|
|
6229
6319
|
/**
|
|
@@ -6235,8 +6325,8 @@ class FieldBuilder {
|
|
|
6235
6325
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6236
6326
|
* want on the class for typing convenience without paying any sync cost.
|
|
6237
6327
|
*
|
|
6238
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6239
|
-
* `.unreliable()`, `.
|
|
6328
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6329
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6240
6330
|
* them throws at `schema()` time.
|
|
6241
6331
|
*
|
|
6242
6332
|
* ```ts
|
|
@@ -6278,9 +6368,12 @@ class FieldBuilder {
|
|
|
6278
6368
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6279
6369
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6280
6370
|
*
|
|
6371
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
6372
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
6373
|
+
*
|
|
6281
6374
|
* ```ts
|
|
6282
6375
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6283
|
-
* -
|
|
6376
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6284
6377
|
* )
|
|
6285
6378
|
* ```
|
|
6286
6379
|
*/
|
|
@@ -6315,12 +6408,11 @@ class FieldBuilder {
|
|
|
6315
6408
|
default: this._default,
|
|
6316
6409
|
hasDefault: this._hasDefault,
|
|
6317
6410
|
view: this._view,
|
|
6318
|
-
owned: this._owned,
|
|
6319
6411
|
unreliable: this._unreliable,
|
|
6320
|
-
|
|
6412
|
+
patchOnly: this._patchOnly,
|
|
6321
6413
|
deprecated: this._deprecated,
|
|
6322
6414
|
deprecatedThrows: this._deprecatedThrows,
|
|
6323
|
-
|
|
6415
|
+
fullStateOnly: this._fullStateOnly,
|
|
6324
6416
|
stream: this._stream,
|
|
6325
6417
|
optional: this._optional,
|
|
6326
6418
|
noSync: this._noSync,
|
|
@@ -6574,25 +6666,44 @@ function view(tag = DEFAULT_VIEW_TAG) {
|
|
|
6574
6666
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6575
6667
|
};
|
|
6576
6668
|
}
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6669
|
+
/**
|
|
6670
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
6671
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
6672
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
6673
|
+
*
|
|
6674
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
6675
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
6676
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
6677
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
6678
|
+
* written again.
|
|
6679
|
+
*/
|
|
6581
6680
|
function unreliable(target, field) {
|
|
6582
6681
|
const metadata = Metadata.initialize(target.constructor);
|
|
6583
6682
|
Metadata.setUnreliable(metadata, field);
|
|
6584
6683
|
}
|
|
6585
6684
|
/**
|
|
6586
|
-
* @
|
|
6587
|
-
* encodeAllView).
|
|
6685
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6686
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6588
6687
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6589
6688
|
* the next mutation.
|
|
6590
6689
|
*
|
|
6591
6690
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6592
6691
|
*/
|
|
6593
|
-
function
|
|
6692
|
+
function patchOnly(target, field) {
|
|
6693
|
+
const metadata = Metadata.initialize(target.constructor);
|
|
6694
|
+
Metadata.setPatchOnly(metadata, field);
|
|
6695
|
+
}
|
|
6696
|
+
/**
|
|
6697
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
6698
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
6699
|
+
* client has joined are not propagated to it — populate these fields
|
|
6700
|
+
* before clients connect (e.g. during onCreate).
|
|
6701
|
+
*
|
|
6702
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
6703
|
+
*/
|
|
6704
|
+
function fullStateOnly(target, field) {
|
|
6594
6705
|
const metadata = Metadata.initialize(target.constructor);
|
|
6595
|
-
Metadata.
|
|
6706
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6596
6707
|
}
|
|
6597
6708
|
function type(type, options) {
|
|
6598
6709
|
return function (target, field) {
|
|
@@ -6964,11 +7075,10 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6964
7075
|
}
|
|
6965
7076
|
};
|
|
6966
7077
|
const viewTagFields = {};
|
|
6967
|
-
const ownedFields = [];
|
|
6968
7078
|
const unreliableFields = [];
|
|
6969
|
-
const
|
|
7079
|
+
const patchOnlyFields = [];
|
|
6970
7080
|
const deprecatedFields = {};
|
|
6971
|
-
const
|
|
7081
|
+
const fullStateOnlyFields = [];
|
|
6972
7082
|
const streamFields = [];
|
|
6973
7083
|
const streamPriorityFields = {};
|
|
6974
7084
|
const optionalFields = [];
|
|
@@ -6980,15 +7090,22 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6980
7090
|
// Local-only field: skip metadata registration entirely so it is
|
|
6981
7091
|
// never encoded/decoded, but still seed its construction default
|
|
6982
7092
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6983
|
-
if (def.view !== undefined || def.
|
|
6984
|
-
def.
|
|
7093
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7094
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6985
7095
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6986
|
-
`together with a sync-only modifier (.view/.
|
|
7096
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6987
7097
|
`A local-only field cannot be synchronized.`);
|
|
6988
7098
|
}
|
|
6989
7099
|
seedDefault(fieldName, def);
|
|
6990
7100
|
continue;
|
|
6991
7101
|
}
|
|
7102
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
7103
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
7104
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
7105
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
7106
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
7107
|
+
`so the field would never reach a client — use .noSync() if that is intended.`);
|
|
7108
|
+
}
|
|
6992
7109
|
const normalizedType = getNormalizedType(def.type);
|
|
6993
7110
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
6994
7111
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -6999,20 +7116,17 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6999
7116
|
if (def.view !== undefined) {
|
|
7000
7117
|
viewTagFields[fieldName] = def.view;
|
|
7001
7118
|
}
|
|
7002
|
-
if (def.owned) {
|
|
7003
|
-
ownedFields.push(fieldName);
|
|
7004
|
-
}
|
|
7005
7119
|
if (def.unreliable) {
|
|
7006
7120
|
unreliableFields.push(fieldName);
|
|
7007
7121
|
}
|
|
7008
|
-
if (def.
|
|
7009
|
-
|
|
7122
|
+
if (def.patchOnly) {
|
|
7123
|
+
patchOnlyFields.push(fieldName);
|
|
7010
7124
|
}
|
|
7011
7125
|
if (def.deprecated) {
|
|
7012
7126
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7013
7127
|
}
|
|
7014
|
-
if (def.
|
|
7015
|
-
|
|
7128
|
+
if (def.fullStateOnly) {
|
|
7129
|
+
fullStateOnlyFields.push(fieldName);
|
|
7016
7130
|
}
|
|
7017
7131
|
if (def.stream) {
|
|
7018
7132
|
streamFields.push(fieldName);
|
|
@@ -7095,22 +7209,19 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
7095
7209
|
for (const fieldName in viewTagFields) {
|
|
7096
7210
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7097
7211
|
}
|
|
7098
|
-
for (const fieldName of ownedFields) {
|
|
7099
|
-
owned(klass.prototype, fieldName);
|
|
7100
|
-
}
|
|
7101
7212
|
for (const fieldName of unreliableFields) {
|
|
7102
7213
|
unreliable(klass.prototype, fieldName);
|
|
7103
7214
|
}
|
|
7104
|
-
for (const fieldName of
|
|
7105
|
-
|
|
7215
|
+
for (const fieldName of patchOnlyFields) {
|
|
7216
|
+
patchOnly(klass.prototype, fieldName);
|
|
7106
7217
|
}
|
|
7107
7218
|
for (const fieldName in deprecatedFields) {
|
|
7108
7219
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7109
7220
|
}
|
|
7110
|
-
if (
|
|
7221
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7111
7222
|
const metadata = klass[Symbol.metadata];
|
|
7112
|
-
for (const fieldName of
|
|
7113
|
-
Metadata.
|
|
7223
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7224
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7114
7225
|
}
|
|
7115
7226
|
for (const fieldName of streamFields) {
|
|
7116
7227
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7759,7 +7870,7 @@ class Root {
|
|
|
7759
7870
|
const previousRefCount = this.refCount[refId];
|
|
7760
7871
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7761
7872
|
//
|
|
7762
|
-
// Re-stage every currently-populated non-
|
|
7873
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7763
7874
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7764
7875
|
// re-emits it on the correct channel. Two triggers:
|
|
7765
7876
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7849,14 +7960,10 @@ class Root {
|
|
|
7849
7960
|
const parentNode = parent[$changes][nodeField];
|
|
7850
7961
|
if (!parentNode || parentNode === node)
|
|
7851
7962
|
return;
|
|
7852
|
-
//
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
return; // already after parent
|
|
7857
|
-
cursor = cursor.next;
|
|
7858
|
-
}
|
|
7859
|
-
// If we reach here, node is before parent — need to move
|
|
7963
|
+
// Positions are strictly increasing along the list, so this is an
|
|
7964
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
7965
|
+
if (node.position > parentNode.position)
|
|
7966
|
+
return;
|
|
7860
7967
|
// Remove node from current position
|
|
7861
7968
|
if (node.prev) {
|
|
7862
7969
|
node.prev.next = node.next;
|
|
@@ -7870,16 +7977,18 @@ class Root {
|
|
|
7870
7977
|
else {
|
|
7871
7978
|
changeSet.tail = node.prev;
|
|
7872
7979
|
}
|
|
7873
|
-
//
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7980
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
7981
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
7982
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
7983
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
7984
|
+
// a fresh max position, keeping the invariant append-only.
|
|
7985
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
7986
|
+
// subtree re-serializes parent-first behind it.)
|
|
7987
|
+
node.prev = changeSet.tail;
|
|
7988
|
+
node.next = undefined;
|
|
7989
|
+
changeSet.tail.next = node; // parentNode remains in the list — never empty here
|
|
7990
|
+
changeSet.tail = node;
|
|
7991
|
+
node.position = changeSet.nextPosition++;
|
|
7883
7992
|
}
|
|
7884
7993
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7885
7994
|
if (existingNode) {
|
|
@@ -7901,12 +8010,12 @@ class Root {
|
|
|
7901
8010
|
node.changeTree = changeTree;
|
|
7902
8011
|
node.next = undefined;
|
|
7903
8012
|
node.prev = undefined;
|
|
7904
|
-
node.position = 0;
|
|
7905
8013
|
}
|
|
7906
8014
|
else {
|
|
7907
8015
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7908
8016
|
}
|
|
7909
8017
|
if (!list.next) {
|
|
8018
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7910
8019
|
list.next = node;
|
|
7911
8020
|
list.tail = node;
|
|
7912
8021
|
}
|
|
@@ -7915,6 +8024,7 @@ class Root {
|
|
|
7915
8024
|
list.tail.next = node;
|
|
7916
8025
|
list.tail = node;
|
|
7917
8026
|
}
|
|
8027
|
+
node.position = list.nextPosition++;
|
|
7918
8028
|
return node;
|
|
7919
8029
|
}
|
|
7920
8030
|
/**
|
|
@@ -8013,6 +8123,7 @@ function _fullSyncWalk(ctx, changeTree) {
|
|
|
8013
8123
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8014
8124
|
ctx.isSchema = desc.isSchema;
|
|
8015
8125
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8126
|
+
ctx.tags = desc.tags;
|
|
8016
8127
|
ctx.structSwitchEmitted = false;
|
|
8017
8128
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8018
8129
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8052,10 +8163,13 @@ function encodeChangeCb(ctx, fieldIndex, op) {
|
|
|
8052
8163
|
}
|
|
8053
8164
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8054
8165
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8055
|
-
// itself carries a @view tag.
|
|
8056
|
-
//
|
|
8166
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
8167
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
8168
|
+
// that arm needs a Schema with more than 32 fields.
|
|
8057
8169
|
const fieldFiltered = ctx.isSchema
|
|
8058
|
-
? (ctx.treeIsFiltered || (
|
|
8170
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8171
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8172
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8059
8173
|
: ctx.treeIsFiltered;
|
|
8060
8174
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8061
8175
|
return;
|
|
@@ -8110,7 +8224,7 @@ class Encoder {
|
|
|
8110
8224
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8111
8225
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8112
8226
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8113
|
-
filterBitmask: 0,
|
|
8227
|
+
filterBitmask: 0, tags: undefined,
|
|
8114
8228
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8115
8229
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8116
8230
|
};
|
|
@@ -8167,6 +8281,7 @@ class Encoder {
|
|
|
8167
8281
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8168
8282
|
ctx.isSchema = desc.isSchema;
|
|
8169
8283
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8284
|
+
ctx.tags = desc.tags;
|
|
8170
8285
|
ctx.structSwitchEmitted = false;
|
|
8171
8286
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8172
8287
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8418,7 +8533,7 @@ class Encoder {
|
|
|
8418
8533
|
// Emit each element's full state — forEachLive walks populated
|
|
8419
8534
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8420
8535
|
// Covers both static elements (dirty state was reset by
|
|
8421
|
-
// inheritedFlags'
|
|
8536
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8422
8537
|
// has dirty state but the main loop skipped them because
|
|
8423
8538
|
// they're filtered).
|
|
8424
8539
|
for (const element of emittedElements) {
|
|
@@ -8497,20 +8612,81 @@ class Encoder {
|
|
|
8497
8612
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8498
8613
|
// into `_stream.priority` when the stream was attached. Users
|
|
8499
8614
|
// can also override per-instance by assigning to the setter.
|
|
8615
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
8616
|
+
// wins over the declaration-scope one: it closes over the
|
|
8617
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
8618
|
+
const perView = st.priorityByView?.get(viewId);
|
|
8619
|
+
const usePerView = perView !== undefined;
|
|
8500
8620
|
const priority = st.priority;
|
|
8501
|
-
|
|
8502
|
-
//
|
|
8503
|
-
//
|
|
8621
|
+
const max = st.maxPerTick;
|
|
8622
|
+
// Select the `max` highest-priority candidates.
|
|
8623
|
+
//
|
|
8624
|
+
// A comparator-based sort invokes the callback twice per
|
|
8625
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
8626
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
8627
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
8628
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
8629
|
+
// the scratch by `max` rather than by the backlog.
|
|
8630
|
+
//
|
|
8631
|
+
// Ties keep the earlier position (both comparisons below are
|
|
8632
|
+
// strict), so equal-priority entries still drain in insertion
|
|
8633
|
+
// order.
|
|
8504
8634
|
const positions = [];
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8635
|
+
const stale = [];
|
|
8636
|
+
if (usePerView || priority !== undefined) {
|
|
8637
|
+
const bestPos = [];
|
|
8638
|
+
const bestScore = [];
|
|
8639
|
+
let filled = 0;
|
|
8640
|
+
for (const pos of pending) {
|
|
8641
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
8642
|
+
// through the same lookup regardless of $items layout.
|
|
8643
|
+
const element = s[$getByIndex](pos);
|
|
8644
|
+
if (element === undefined) {
|
|
8645
|
+
// Removed after being queued — drop it below without
|
|
8646
|
+
// spending budget on it.
|
|
8647
|
+
stale.push(pos);
|
|
8648
|
+
continue;
|
|
8649
|
+
}
|
|
8650
|
+
const score = usePerView
|
|
8651
|
+
? perView(element)
|
|
8652
|
+
: priority(view, element);
|
|
8653
|
+
// Window not yet full: always insert.
|
|
8654
|
+
if (filled < max) {
|
|
8655
|
+
let j = filled++;
|
|
8656
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8657
|
+
bestScore[j] = bestScore[j - 1];
|
|
8658
|
+
bestPos[j] = bestPos[j - 1];
|
|
8659
|
+
j--;
|
|
8660
|
+
}
|
|
8661
|
+
bestScore[j] = score;
|
|
8662
|
+
bestPos[j] = pos;
|
|
8663
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
8664
|
+
}
|
|
8665
|
+
else if (score > bestScore[max - 1]) {
|
|
8666
|
+
let j = max - 1;
|
|
8667
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8668
|
+
bestScore[j] = bestScore[j - 1];
|
|
8669
|
+
bestPos[j] = bestPos[j - 1];
|
|
8670
|
+
j--;
|
|
8671
|
+
}
|
|
8672
|
+
bestScore[j] = score;
|
|
8673
|
+
bestPos[j] = pos;
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8676
|
+
for (let i = 0; i < filled; i++)
|
|
8677
|
+
positions.push(bestPos[i]);
|
|
8511
8678
|
}
|
|
8512
|
-
|
|
8513
|
-
|
|
8679
|
+
else {
|
|
8680
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
8681
|
+
for (const pos of pending) {
|
|
8682
|
+
if (positions.length >= max)
|
|
8683
|
+
break;
|
|
8684
|
+
positions.push(pos);
|
|
8685
|
+
}
|
|
8686
|
+
}
|
|
8687
|
+
for (const pos of stale)
|
|
8688
|
+
pending.delete(pos);
|
|
8689
|
+
const count = positions.length;
|
|
8514
8690
|
let sent = st.sentByView.get(viewId);
|
|
8515
8691
|
if (sent === undefined) {
|
|
8516
8692
|
sent = new Set();
|
|
@@ -9865,6 +10041,31 @@ const _disposeRegistry = new FinalizationRegistry(({ root, id, slot, bit }) => {
|
|
|
9865
10041
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9866
10042
|
root.releaseViewId(id);
|
|
9867
10043
|
});
|
|
10044
|
+
/**
|
|
10045
|
+
* Compact description of a rejected argument, for warning messages.
|
|
10046
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
10047
|
+
* populated collection inspects into dozens of lines of encoder
|
|
10048
|
+
* internals and buries the message that matters.
|
|
10049
|
+
*/
|
|
10050
|
+
function describeArg(value) {
|
|
10051
|
+
if (value === undefined) {
|
|
10052
|
+
return "undefined";
|
|
10053
|
+
}
|
|
10054
|
+
if (value === null) {
|
|
10055
|
+
return "null";
|
|
10056
|
+
}
|
|
10057
|
+
const type = typeof value;
|
|
10058
|
+
if (type === "string") {
|
|
10059
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
10060
|
+
}
|
|
10061
|
+
if (type !== "object" && type !== "function") {
|
|
10062
|
+
return `${type} ${String(value)}`;
|
|
10063
|
+
}
|
|
10064
|
+
if (Array.isArray(value)) {
|
|
10065
|
+
return `Array(${value.length})`;
|
|
10066
|
+
}
|
|
10067
|
+
return value.constructor?.name ?? "Object";
|
|
10068
|
+
}
|
|
9868
10069
|
class StateView {
|
|
9869
10070
|
iterable;
|
|
9870
10071
|
/**
|
|
@@ -10089,12 +10290,12 @@ class StateView {
|
|
|
10089
10290
|
}
|
|
10090
10291
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10091
10292
|
const changeTree = obj?.[$changes];
|
|
10092
|
-
const parentChangeTree = changeTree.parent;
|
|
10093
10293
|
if (!changeTree) {
|
|
10094
|
-
console.warn(
|
|
10294
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10095
10295
|
return false;
|
|
10096
10296
|
}
|
|
10097
|
-
|
|
10297
|
+
const parentChangeTree = changeTree.parent;
|
|
10298
|
+
if (!parentChangeTree &&
|
|
10098
10299
|
obj[$refId] !== 0 // allow root object
|
|
10099
10300
|
) {
|
|
10100
10301
|
/**
|
|
@@ -10375,9 +10576,9 @@ class StateView {
|
|
|
10375
10576
|
}
|
|
10376
10577
|
}
|
|
10377
10578
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10378
|
-
const changeTree = obj[$changes];
|
|
10579
|
+
const changeTree = obj?.[$changes];
|
|
10379
10580
|
if (!changeTree) {
|
|
10380
|
-
console.warn(
|
|
10581
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10381
10582
|
return this;
|
|
10382
10583
|
}
|
|
10383
10584
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10515,32 +10716,47 @@ class StateView {
|
|
|
10515
10716
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10516
10717
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10517
10718
|
}
|
|
10518
|
-
|
|
10519
|
-
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
10520
|
-
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
10521
|
-
* future content changes — every subsequent push / set / add to the
|
|
10522
|
-
* collection automatically flows to this view, and every removal
|
|
10523
|
-
* queues a DELETE op. Works on every collection type:
|
|
10524
|
-
*
|
|
10525
|
-
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
10526
|
-
* new children are force-shipped immediately (equivalent to
|
|
10527
|
-
* `view.add(child)` per item).
|
|
10528
|
-
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
10529
|
-
* enqueued into `_pendingByView` so the priority pass drains them
|
|
10530
|
-
* respecting `maxPerTick`.
|
|
10531
|
-
*
|
|
10532
|
-
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
10533
|
-
* collection is a no-op.
|
|
10534
|
-
*/
|
|
10535
|
-
subscribe(collection) {
|
|
10719
|
+
subscribe(collection, priority) {
|
|
10536
10720
|
const tree = collection?.[$changes];
|
|
10537
10721
|
if (!tree) {
|
|
10538
|
-
console.warn(
|
|
10722
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10539
10723
|
return this;
|
|
10540
10724
|
}
|
|
10541
10725
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10542
10726
|
this._bindRoot(tree.root);
|
|
10543
10727
|
}
|
|
10728
|
+
if (priority !== undefined) {
|
|
10729
|
+
if (!tree.isStreamCollection) {
|
|
10730
|
+
// Name the field rather than dumping the collection — a
|
|
10731
|
+
// populated MapSchema inspects into dozens of lines of
|
|
10732
|
+
// internals and buries the message.
|
|
10733
|
+
const kind = collection?.constructor?.name ?? "collection";
|
|
10734
|
+
const parent = tree.parent;
|
|
10735
|
+
if (parent === undefined) {
|
|
10736
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
10737
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
10738
|
+
`Subscribe after assigning it to the state.`);
|
|
10739
|
+
}
|
|
10740
|
+
else {
|
|
10741
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
10742
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
10743
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
10744
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
10745
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`);
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10748
|
+
else {
|
|
10749
|
+
// Set before the idempotency return below, so re-subscribing
|
|
10750
|
+
// is the documented way to retarget this view's ordering.
|
|
10751
|
+
const st = ensureStreamState(collection);
|
|
10752
|
+
if (priority === null) {
|
|
10753
|
+
st.priorityByView?.delete(this.id);
|
|
10754
|
+
}
|
|
10755
|
+
else {
|
|
10756
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10544
10760
|
if (this.isSubscribed(tree))
|
|
10545
10761
|
return this;
|
|
10546
10762
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10586,7 +10802,7 @@ class StateView {
|
|
|
10586
10802
|
unsubscribe(collection) {
|
|
10587
10803
|
const tree = collection?.[$changes];
|
|
10588
10804
|
if (!tree) {
|
|
10589
|
-
console.warn(
|
|
10805
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10590
10806
|
return this;
|
|
10591
10807
|
}
|
|
10592
10808
|
if (!this.isSubscribed(tree))
|
|
@@ -10670,5 +10886,5 @@ registerType("array", { constructor: ArraySchema });
|
|
|
10670
10886
|
registerType("set", { constructor: SetSchema });
|
|
10671
10887
|
registerType("collection", { constructor: CollectionSchema, });
|
|
10672
10888
|
|
|
10673
|
-
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,
|
|
10889
|
+
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 };
|
|
10674
10890
|
//# sourceMappingURL=index.mjs.map
|