@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.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const SWITCH_TO_STRUCTURE = 255; //
|
|
3
|
+
const SWITCH_TO_STRUCTURE = 255; // same byte as `DELETE_AND_ADD | 63`, which is why field index 63 is unassignable (Metadata.MAX_FIELDS)
|
|
4
4
|
const TYPE_ID = 213;
|
|
5
5
|
/**
|
|
6
6
|
* Encoding Schema field operations.
|
|
@@ -120,16 +120,10 @@ const $builder = "~builder";
|
|
|
120
120
|
* Metadata
|
|
121
121
|
*/
|
|
122
122
|
const $descriptors = "~descriptors";
|
|
123
|
-
/**
|
|
124
|
-
* Per-class bitmask: bit i set iff field i carries a @view tag.
|
|
125
|
-
* Lazily computed from $viewFieldIndexes on first encode pass.
|
|
126
|
-
* Skips the per-field metadata[i].tag property chase in the hot encode loop.
|
|
127
|
-
*/
|
|
128
|
-
const $filterBitmask = "~__filterBitmask";
|
|
129
123
|
/**
|
|
130
124
|
* Cached per-class encode descriptor: bundles encoder fn, filter fn,
|
|
131
|
-
* metadata, isSchema flag
|
|
132
|
-
* the constructor. Replaces
|
|
125
|
+
* metadata, isSchema flag and the per-field arrays into one object stashed
|
|
126
|
+
* on the constructor. Replaces several separate per-tree property chases /
|
|
133
127
|
* function calls in the encode loop with a single property load.
|
|
134
128
|
*/
|
|
135
129
|
const $encodeDescriptor = "~__encodeDescriptor";
|
|
@@ -139,8 +133,8 @@ const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
|
|
|
139
133
|
const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
140
134
|
const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
141
135
|
const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
142
|
-
const $
|
|
143
|
-
const $
|
|
136
|
+
const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
137
|
+
const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
144
138
|
const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
145
139
|
const $streamPriorities = "~__streamPriorities";
|
|
146
140
|
|
|
@@ -821,6 +815,7 @@ function streamDropView(s, viewId) {
|
|
|
821
815
|
return;
|
|
822
816
|
st.pendingByView.delete(viewId);
|
|
823
817
|
st.sentByView.delete(viewId);
|
|
818
|
+
st.priorityByView?.delete(viewId);
|
|
824
819
|
}
|
|
825
820
|
|
|
826
821
|
const WIRE_BY_BITS = {
|
|
@@ -1066,6 +1061,15 @@ class TypeContext {
|
|
|
1066
1061
|
}
|
|
1067
1062
|
}
|
|
1068
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Field indexes ride in the low 6 bits of the operation byte
|
|
1066
|
+
* (`(index | operation) & 255`), which leaves room for 0..63. Index 63 is
|
|
1067
|
+
* given up: `DELETE_AND_ADD | 63` is 255, the same byte the decoder claims
|
|
1068
|
+
* as SWITCH_TO_STRUCTURE before any field decoder sees it. Every nullable
|
|
1069
|
+
* field can produce that operation (delete-then-set in one tick merges to
|
|
1070
|
+
* DELETE_AND_ADD), so the slot is unusable rather than partly usable.
|
|
1071
|
+
*/
|
|
1072
|
+
const MAX_FIELDS = 63;
|
|
1069
1073
|
/**
|
|
1070
1074
|
* Given a normalized field type (`"number"`, `{ map: Foo }`, `Player`,
|
|
1071
1075
|
* etc.), split into the collection-type descriptor (`{ constructor:
|
|
@@ -1129,10 +1133,12 @@ function isTSEnum(_enum) {
|
|
|
1129
1133
|
}
|
|
1130
1134
|
const Metadata = {
|
|
1131
1135
|
addField(metadata, index, name, type, descriptor) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1136
|
+
// `index` is 0-based, so 62 is the last usable slot — see MAX_FIELDS
|
|
1137
|
+
// for why 63 is off limits.
|
|
1138
|
+
if (index >= MAX_FIELDS) {
|
|
1139
|
+
throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to ${MAX_FIELDS} fields.`);
|
|
1134
1140
|
}
|
|
1135
|
-
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@
|
|
1141
|
+
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@deprecated / @unreliable)
|
|
1136
1142
|
{
|
|
1137
1143
|
type: getNormalizedType(type),
|
|
1138
1144
|
index,
|
|
@@ -1282,31 +1288,43 @@ const Metadata = {
|
|
|
1282
1288
|
}
|
|
1283
1289
|
metadata[$unreliableFieldIndexes].push(index);
|
|
1284
1290
|
},
|
|
1285
|
-
|
|
1291
|
+
setPatchOnly(metadata, fieldName) {
|
|
1286
1292
|
const index = metadata[fieldName];
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1293
|
+
// patchOnly + fullStateOnly are the only two delivery channels —
|
|
1294
|
+
// excluding a field from both would silently never reach a client.
|
|
1295
|
+
// (The builder validates earlier; this guards the decorator path.)
|
|
1296
|
+
if (metadata[index].fullStateOnly) {
|
|
1297
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1298
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1299
|
+
}
|
|
1300
|
+
metadata[index].patchOnly = true;
|
|
1301
|
+
if (!metadata[$patchOnlyFieldIndexes]) {
|
|
1302
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1290
1303
|
value: [],
|
|
1291
1304
|
enumerable: false,
|
|
1292
1305
|
configurable: true,
|
|
1293
1306
|
writable: true,
|
|
1294
1307
|
});
|
|
1295
1308
|
}
|
|
1296
|
-
metadata[$
|
|
1309
|
+
metadata[$patchOnlyFieldIndexes].push(index);
|
|
1297
1310
|
},
|
|
1298
|
-
|
|
1311
|
+
setFullStateOnly(metadata, fieldName) {
|
|
1299
1312
|
const index = metadata[fieldName];
|
|
1300
|
-
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1313
|
+
// Mirror of the guard in setPatchOnly — covers both decorator orders.
|
|
1314
|
+
if (metadata[index].patchOnly) {
|
|
1315
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1316
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1317
|
+
}
|
|
1318
|
+
metadata[index].fullStateOnly = true;
|
|
1319
|
+
if (!metadata[$fullStateOnlyFieldIndexes]) {
|
|
1320
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1303
1321
|
value: [],
|
|
1304
1322
|
enumerable: false,
|
|
1305
1323
|
configurable: true,
|
|
1306
1324
|
writable: true,
|
|
1307
1325
|
});
|
|
1308
1326
|
}
|
|
1309
|
-
metadata[$
|
|
1327
|
+
metadata[$fullStateOnlyFieldIndexes].push(index);
|
|
1310
1328
|
},
|
|
1311
1329
|
setStream(metadata, fieldName) {
|
|
1312
1330
|
const index = metadata[fieldName];
|
|
@@ -1470,19 +1488,19 @@ const Metadata = {
|
|
|
1470
1488
|
writable: true,
|
|
1471
1489
|
});
|
|
1472
1490
|
}
|
|
1473
|
-
// $
|
|
1474
|
-
if (parentMetadata[$
|
|
1475
|
-
Object.defineProperty(metadata, $
|
|
1476
|
-
value: [...parentMetadata[$
|
|
1491
|
+
// $patchOnlyFieldIndexes
|
|
1492
|
+
if (parentMetadata[$patchOnlyFieldIndexes] !== undefined) {
|
|
1493
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1494
|
+
value: [...parentMetadata[$patchOnlyFieldIndexes]],
|
|
1477
1495
|
enumerable: false,
|
|
1478
1496
|
configurable: true,
|
|
1479
1497
|
writable: true,
|
|
1480
1498
|
});
|
|
1481
1499
|
}
|
|
1482
|
-
// $
|
|
1483
|
-
if (parentMetadata[$
|
|
1484
|
-
Object.defineProperty(metadata, $
|
|
1485
|
-
value: [...parentMetadata[$
|
|
1500
|
+
// $fullStateOnlyFieldIndexes
|
|
1501
|
+
if (parentMetadata[$fullStateOnlyFieldIndexes] !== undefined) {
|
|
1502
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1503
|
+
value: [...parentMetadata[$fullStateOnlyFieldIndexes]],
|
|
1486
1504
|
enumerable: false,
|
|
1487
1505
|
configurable: true,
|
|
1488
1506
|
writable: true,
|
|
@@ -1540,11 +1558,11 @@ const Metadata = {
|
|
|
1540
1558
|
hasUnreliableAtIndex(metadata, index) {
|
|
1541
1559
|
return metadata?.[$unreliableFieldIndexes]?.includes(index);
|
|
1542
1560
|
},
|
|
1543
|
-
|
|
1544
|
-
return metadata?.[$
|
|
1561
|
+
hasPatchOnlyAtIndex(metadata, index) {
|
|
1562
|
+
return metadata?.[$patchOnlyFieldIndexes]?.includes(index);
|
|
1545
1563
|
},
|
|
1546
|
-
|
|
1547
|
-
return metadata?.[$
|
|
1564
|
+
hasFullStateOnlyAtIndex(metadata, index) {
|
|
1565
|
+
return metadata?.[$fullStateOnlyFieldIndexes]?.includes(index);
|
|
1548
1566
|
},
|
|
1549
1567
|
hasStreamAtIndex(metadata, index) {
|
|
1550
1568
|
return metadata?.[$streamFieldIndexes]?.includes(index);
|
|
@@ -1555,7 +1573,7 @@ const Metadata = {
|
|
|
1555
1573
|
// by passing the user's callback as ctx. No per-call allocation.
|
|
1556
1574
|
const _invokeNoCtx$2 = (cb, index, op) => cb(index, op);
|
|
1557
1575
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1558
|
-
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤
|
|
1576
|
+
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤63 fields)
|
|
1559
1577
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1560
1578
|
/**
|
|
1561
1579
|
* Schema field operations are limited to ADD(128), DELETE(64), and
|
|
@@ -1748,37 +1766,15 @@ function popcount32(n) {
|
|
|
1748
1766
|
* ctor[$filter]
|
|
1749
1767
|
* ctor[Symbol.metadata]
|
|
1750
1768
|
* Metadata.isValidInstance(ref)
|
|
1751
|
-
* getFilterBitmask(metadata)
|
|
1752
1769
|
*
|
|
1753
1770
|
* Lives in its own file to break the Encoder.ts ↔ ChangeTree.ts import
|
|
1754
1771
|
* cycle (ChangeTree caches descriptors at construction; Encoder reads them
|
|
1755
1772
|
* during encode).
|
|
1756
1773
|
*/
|
|
1757
|
-
function computeFilterBitmask(metadata) {
|
|
1758
|
-
if (metadata === undefined)
|
|
1759
|
-
return 0;
|
|
1760
|
-
let bm = metadata[$filterBitmask];
|
|
1761
|
-
if (bm !== undefined)
|
|
1762
|
-
return bm;
|
|
1763
|
-
bm = 0;
|
|
1764
|
-
const tagged = metadata[$viewFieldIndexes];
|
|
1765
|
-
if (tagged !== undefined) {
|
|
1766
|
-
for (let i = 0, len = tagged.length; i < len; i++)
|
|
1767
|
-
bm |= (1 << tagged[i]);
|
|
1768
|
-
}
|
|
1769
|
-
// Non-enumerable so `for (const k in metadata)` iteration in TypeContext
|
|
1770
|
-
// and elsewhere doesn't mistake this cache for a real field index.
|
|
1771
|
-
Object.defineProperty(metadata, $filterBitmask, {
|
|
1772
|
-
value: bm,
|
|
1773
|
-
enumerable: false,
|
|
1774
|
-
writable: true,
|
|
1775
|
-
configurable: true,
|
|
1776
|
-
});
|
|
1777
|
-
return bm;
|
|
1778
|
-
}
|
|
1779
1774
|
/**
|
|
1780
1775
|
* Bitmask of field indexes 0–31 in `indexes`. For fields ≥32 callers must
|
|
1781
|
-
* fall back to the array lookup
|
|
1776
|
+
* fall back to the array lookup — shift counts wrap at 32, so an unguarded
|
|
1777
|
+
* `1 << 40` would set bit 8 and misclassify field 8.
|
|
1782
1778
|
*/
|
|
1783
1779
|
function indexesToBitmask(indexes) {
|
|
1784
1780
|
if (indexes === undefined)
|
|
@@ -1854,12 +1850,12 @@ function getEncodeDescriptor(ref) {
|
|
|
1854
1850
|
filter,
|
|
1855
1851
|
metadata,
|
|
1856
1852
|
isSchema,
|
|
1857
|
-
filterBitmask: isSchema ?
|
|
1858
|
-
|
|
1853
|
+
filterBitmask: isSchema ? indexesToBitmask(metadata?.[$viewFieldIndexes]) : 0,
|
|
1854
|
+
hasAnyFullStateOnly: (metadata?.[$fullStateOnlyFieldIndexes]?.length ?? 0) > 0,
|
|
1859
1855
|
hasAnyUnreliable: (metadata?.[$unreliableFieldIndexes]?.length ?? 0) > 0,
|
|
1860
1856
|
hasAnyStream: (metadata?.[$streamFieldIndexes]?.length ?? 0) > 0,
|
|
1861
1857
|
hasAnyView,
|
|
1862
|
-
|
|
1858
|
+
fullStateOnlyBitmask: indexesToBitmask(metadata?.[$fullStateOnlyFieldIndexes]),
|
|
1863
1859
|
unreliableBitmask: indexesToBitmask(metadata?.[$unreliableFieldIndexes]),
|
|
1864
1860
|
streamBitmask: indexesToBitmask(metadata?.[$streamFieldIndexes]),
|
|
1865
1861
|
names: arrays.names,
|
|
@@ -1918,6 +1914,34 @@ function addParent(tree, parent, index) {
|
|
|
1918
1914
|
tree._parentIndex = index;
|
|
1919
1915
|
}
|
|
1920
1916
|
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Move `parent`'s existing chain entry to `index`, skipping the attachment
|
|
1919
|
+
* work `addParent` does. `parent` must already be a parent of `tree`.
|
|
1920
|
+
*
|
|
1921
|
+
* Called by collections whose wire slots shift (ArraySchema): StateView
|
|
1922
|
+
* addresses per-view ADD/DELETE by that index, so it has to follow the
|
|
1923
|
+
* element it names.
|
|
1924
|
+
*/
|
|
1925
|
+
function setParentIndex(tree, parent, index) {
|
|
1926
|
+
if (tree.extraParents === undefined) {
|
|
1927
|
+
tree._parentIndex = index; // sole parent, so it is `parent`
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
// Shared instance — move only the entry `parent` owns. Matching goes
|
|
1931
|
+
// through `$changes` because ArraySchema arrives proxied (see removeParent
|
|
1932
|
+
// below), and `extraParents` only ever fills by demoting `parentRef`, so
|
|
1933
|
+
// the inline parent is set here.
|
|
1934
|
+
if (tree.parentRef[$changes] === parent[$changes]) {
|
|
1935
|
+
tree._parentIndex = index;
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
1939
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
1940
|
+
entry.index = index;
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1921
1945
|
/**
|
|
1922
1946
|
* Remove a parent from the chain.
|
|
1923
1947
|
* @returns true if parent was found and removed (Root.remove relies on this).
|
|
@@ -1995,13 +2019,13 @@ function getAllParents(tree) {
|
|
|
1995
2019
|
}
|
|
1996
2020
|
|
|
1997
2021
|
/**
|
|
1998
|
-
* Walk all currently-populated non-
|
|
2022
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
1999
2023
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
2000
2024
|
* StateView.add to derive full-sync output from the live structure.
|
|
2001
2025
|
*
|
|
2002
|
-
*
|
|
2026
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
2003
2027
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
2004
|
-
* field is @
|
|
2028
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
2005
2029
|
*/
|
|
2006
2030
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
2007
2031
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
@@ -2015,10 +2039,10 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2015
2039
|
// types. See `ChangeTree.refTarget` doc.
|
|
2016
2040
|
const ref = tree.refTarget;
|
|
2017
2041
|
if (ref[$childType] !== undefined) {
|
|
2018
|
-
// Collection inheriting @
|
|
2042
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
2019
2043
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
2020
2044
|
// absent from full-sync output is never pruned client-side.
|
|
2021
|
-
if (tree.
|
|
2045
|
+
if (tree.isPatchOnly)
|
|
2022
2046
|
return;
|
|
2023
2047
|
// Collection types: dispatch by shape.
|
|
2024
2048
|
if (Array.isArray(ref.items)) {
|
|
@@ -2047,7 +2071,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2047
2071
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
2048
2072
|
// the setter records a DELETE when a field is set to null or
|
|
2049
2073
|
// undefined, so it should not appear in full-sync output.
|
|
2050
|
-
// (@
|
|
2074
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
2051
2075
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
2052
2076
|
//
|
|
2053
2077
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -2057,13 +2081,13 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2057
2081
|
if (!metadata)
|
|
2058
2082
|
return;
|
|
2059
2083
|
const numFields = (metadata[$numFields] ?? -1);
|
|
2060
|
-
const
|
|
2084
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
2061
2085
|
const names = tree.encDescriptor.names;
|
|
2062
2086
|
for (let i = 0; i <= numFields; i++) {
|
|
2063
2087
|
const name = names[i];
|
|
2064
2088
|
if (name === undefined)
|
|
2065
2089
|
continue;
|
|
2066
|
-
if (
|
|
2090
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i))
|
|
2067
2091
|
continue;
|
|
2068
2092
|
const value = ref[name];
|
|
2069
2093
|
if (value !== undefined && value !== null)
|
|
@@ -2073,7 +2097,7 @@ function forEachLiveWithCtx(tree, ctx, cb) {
|
|
|
2073
2097
|
}
|
|
2074
2098
|
|
|
2075
2099
|
/**
|
|
2076
|
-
* Filter / unreliable /
|
|
2100
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
2077
2101
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
2078
2102
|
* the parent field's annotation + the parent tree's own state.
|
|
2079
2103
|
*/
|
|
@@ -2086,7 +2110,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2086
2110
|
checkInheritedFlags(tree, parent, parentIndex);
|
|
2087
2111
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
2088
2112
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
2089
|
-
if (tree.
|
|
2113
|
+
if (tree.isFullStateOnly)
|
|
2090
2114
|
return;
|
|
2091
2115
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
2092
2116
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -2112,7 +2136,7 @@ function checkIsFiltered(tree, parent, parentIndex, _isNewChangeTree) {
|
|
|
2112
2136
|
}
|
|
2113
2137
|
}
|
|
2114
2138
|
/**
|
|
2115
|
-
* Inherit filter / unreliable /
|
|
2139
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
2116
2140
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
2117
2141
|
* etc.) inherit these from the Schema field that holds them.
|
|
2118
2142
|
*
|
|
@@ -2151,14 +2175,14 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2151
2175
|
parentIndex = parentChangeTree.parentIndex;
|
|
2152
2176
|
}
|
|
2153
2177
|
const parentMetadata = parent?.constructor?.[Symbol.metadata];
|
|
2154
|
-
// Flag inheritance — pack the
|
|
2178
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
2155
2179
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
2156
2180
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
2157
2181
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
2158
2182
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
2159
2183
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
2160
|
-
const fieldBits = (parentMetadata?.[$
|
|
2161
|
-
| (parentMetadata?.[$
|
|
2184
|
+
const fieldBits = (parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
2185
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
2162
2186
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
2163
2187
|
const beforeFlags = tree.flags;
|
|
2164
2188
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -2168,7 +2192,7 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2168
2192
|
// `new Config().assign({...})` populates the recorder before the
|
|
2169
2193
|
// Config instance is attached). Static trees ship state via structural
|
|
2170
2194
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
2171
|
-
if (gainedBits &
|
|
2195
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
2172
2196
|
tree.reset();
|
|
2173
2197
|
tree.unreliableRecorder?.reset();
|
|
2174
2198
|
}
|
|
@@ -2473,7 +2497,7 @@ function _setParentChildCb(ctx, child, index) {
|
|
|
2473
2497
|
*
|
|
2474
2498
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
2475
2499
|
* - liveIteration.ts forEachLive
|
|
2476
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
2500
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
2477
2501
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
2478
2502
|
*
|
|
2479
2503
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -2493,12 +2517,12 @@ function readInlineOpByte(low, high, index) {
|
|
|
2493
2517
|
const _invokeNoCtx = (cb, index, op) => cb(index, op);
|
|
2494
2518
|
// Linked list helper functions
|
|
2495
2519
|
function createChangeTreeList() {
|
|
2496
|
-
return { next: undefined, tail: undefined };
|
|
2520
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
2497
2521
|
}
|
|
2498
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
2522
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
2499
2523
|
// field's annotation — inherited at setParent/setRoot time.
|
|
2500
2524
|
const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
2501
|
-
const IS_UNRELIABLE = 8,
|
|
2525
|
+
const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
2502
2526
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
2503
2527
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
2504
2528
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -2525,7 +2549,7 @@ const NEEDS_RESTAGE = 128;
|
|
|
2525
2549
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
2526
2550
|
* field mutations only) is designed later.
|
|
2527
2551
|
*/
|
|
2528
|
-
const INHERITABLE_FLAGS =
|
|
2552
|
+
const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
2529
2553
|
class ChangeTree {
|
|
2530
2554
|
ref;
|
|
2531
2555
|
/**
|
|
@@ -2544,8 +2568,8 @@ class ChangeTree {
|
|
|
2544
2568
|
refTarget;
|
|
2545
2569
|
metadata;
|
|
2546
2570
|
/**
|
|
2547
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
2548
|
-
*
|
|
2571
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
2572
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
2549
2573
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
2550
2574
|
* EncodeDescriptor.ts.
|
|
2551
2575
|
*/
|
|
@@ -2616,10 +2640,10 @@ class ChangeTree {
|
|
|
2616
2640
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2617
2641
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2618
2642
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2619
|
-
get
|
|
2620
|
-
set
|
|
2621
|
-
get
|
|
2622
|
-
set
|
|
2643
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
2644
|
+
set isPatchOnly(v) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
2645
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
2646
|
+
set isFullStateOnly(v) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
2623
2647
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2624
2648
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2625
2649
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2628,7 +2652,7 @@ class ChangeTree {
|
|
|
2628
2652
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2629
2653
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2630
2654
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2631
|
-
// pattern as `
|
|
2655
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2632
2656
|
get hasFilteredFields() {
|
|
2633
2657
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2634
2658
|
}
|
|
@@ -2652,7 +2676,7 @@ class ChangeTree {
|
|
|
2652
2676
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2653
2677
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2654
2678
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2655
|
-
// (
|
|
2679
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2656
2680
|
const desc = this.encDescriptor;
|
|
2657
2681
|
if (!desc.hasAnyUnreliable)
|
|
2658
2682
|
return false;
|
|
@@ -2662,15 +2686,15 @@ class ChangeTree {
|
|
|
2662
2686
|
}
|
|
2663
2687
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2664
2688
|
// by the tracker (the value still lives on the instance).
|
|
2665
|
-
|
|
2666
|
-
if (this.
|
|
2689
|
+
isFieldFullStateOnly(index) {
|
|
2690
|
+
if (this.isFullStateOnly)
|
|
2667
2691
|
return true;
|
|
2668
2692
|
const desc = this.encDescriptor;
|
|
2669
|
-
if (!desc.
|
|
2693
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2670
2694
|
return false;
|
|
2671
2695
|
if (index < 32)
|
|
2672
|
-
return (desc.
|
|
2673
|
-
return Metadata.
|
|
2696
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2697
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2674
2698
|
}
|
|
2675
2699
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2676
2700
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2929,7 +2953,7 @@ class ChangeTree {
|
|
|
2929
2953
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2930
2954
|
this.unreliableRecorder?.reset();
|
|
2931
2955
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2932
|
-
// (FILTERED/
|
|
2956
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2933
2957
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2934
2958
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2935
2959
|
this._fullSyncGen = 0;
|
|
@@ -2962,7 +2986,7 @@ class ChangeTree {
|
|
|
2962
2986
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2963
2987
|
const src = this.collDirty;
|
|
2964
2988
|
const dst = new Map();
|
|
2965
|
-
const track = !this.paused && !this.
|
|
2989
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2966
2990
|
if (track) {
|
|
2967
2991
|
for (let i = 0; i < count; i++)
|
|
2968
2992
|
dst.set(i, exports.OPERATION.ADD);
|
|
@@ -2986,7 +3010,7 @@ class ChangeTree {
|
|
|
2986
3010
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2987
3011
|
}
|
|
2988
3012
|
operation(op) {
|
|
2989
|
-
if (this.paused || this.
|
|
3013
|
+
if (this.paused || this.isFullStateOnly)
|
|
2990
3014
|
return;
|
|
2991
3015
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2992
3016
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3016,11 +3040,23 @@ class ChangeTree {
|
|
|
3016
3040
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3017
3041
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3018
3042
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
3043
|
+
*
|
|
3044
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
3045
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
3046
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
3047
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
3048
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
3049
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
3050
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
3051
|
+
* which has always seeded these fields for late joiners.
|
|
3052
|
+
*
|
|
3053
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
3054
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
3019
3055
|
*/
|
|
3020
3056
|
_routeAndRecord(index, op, raw) {
|
|
3021
|
-
if (this.paused || this.
|
|
3057
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3022
3058
|
return;
|
|
3023
|
-
if (this.isFieldUnreliable(index)) {
|
|
3059
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3024
3060
|
const r = this.ensureUnreliableRecorder();
|
|
3025
3061
|
if (raw)
|
|
3026
3062
|
r.recordRaw(index, op);
|
|
@@ -3087,9 +3123,11 @@ class ChangeTree {
|
|
|
3087
3123
|
}
|
|
3088
3124
|
return;
|
|
3089
3125
|
}
|
|
3090
|
-
if (this.paused || this.
|
|
3126
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3091
3127
|
return this.getValue(index);
|
|
3092
|
-
|
|
3128
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
3129
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
3130
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
3093
3131
|
if (unreliable)
|
|
3094
3132
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? exports.OPERATION.DELETE);
|
|
3095
3133
|
else
|
|
@@ -3148,6 +3186,8 @@ class ChangeTree {
|
|
|
3148
3186
|
get parent() { return this.parentRef; }
|
|
3149
3187
|
get parentIndex() { return this._parentIndex; }
|
|
3150
3188
|
addParent(parent, index) { addParent(this, parent, index); }
|
|
3189
|
+
/** Re-point an existing parent's cached index after the parent reindexed. */
|
|
3190
|
+
setParentIndex(parent, index) { setParentIndex(this, parent, index); }
|
|
3151
3191
|
/** @returns true if parent was found and removed */
|
|
3152
3192
|
removeParent(parent = this.parent) { return removeParent(this, parent); }
|
|
3153
3193
|
findParent(predicate) {
|
|
@@ -3192,6 +3232,7 @@ class UntrackedChangeTree {
|
|
|
3192
3232
|
operation() { }
|
|
3193
3233
|
setParent() { }
|
|
3194
3234
|
addParent() { }
|
|
3235
|
+
setParentIndex() { }
|
|
3195
3236
|
removeParent() { return false; }
|
|
3196
3237
|
getChange() { return 0; }
|
|
3197
3238
|
discard() { }
|
|
@@ -3295,7 +3336,9 @@ function encodeValue(encoder, bytes, type, value, operation, it, encoderFn) {
|
|
|
3295
3336
|
* @private
|
|
3296
3337
|
*/
|
|
3297
3338
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3298
|
-
// "compress" field index + operation
|
|
3339
|
+
// "compress" field index + operation. Can't collide with
|
|
3340
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
3341
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
3299
3342
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3300
3343
|
// Do not encode value for DELETE operations
|
|
3301
3344
|
if (operation === exports.OPERATION.DELETE) {
|
|
@@ -3491,8 +3534,8 @@ function resyncTouchEntry(decoder, ref, operation, identity, previousValue, valu
|
|
|
3491
3534
|
/**
|
|
3492
3535
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3493
3536
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3494
|
-
* of full-sync" (@
|
|
3495
|
-
* live data. Reflected clients have no @
|
|
3537
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
3538
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
3496
3539
|
* presence is the only reliable signal.
|
|
3497
3540
|
*/
|
|
3498
3541
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3506,7 +3549,7 @@ function resyncMarkPresent(decoder, refId) {
|
|
|
3506
3549
|
* entry the snapshot did not visit.
|
|
3507
3550
|
*
|
|
3508
3551
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3509
|
-
* `@
|
|
3552
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3510
3553
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3511
3554
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3512
3555
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3531,11 +3574,11 @@ function sweepSchema(decoder, ref, seen, allChanges) {
|
|
|
3531
3574
|
if (refIndexes === undefined) {
|
|
3532
3575
|
return;
|
|
3533
3576
|
}
|
|
3534
|
-
const
|
|
3577
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3535
3578
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3536
3579
|
const fieldIndex = refIndexes[i];
|
|
3537
|
-
// @
|
|
3538
|
-
if (
|
|
3580
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3581
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3539
3582
|
continue;
|
|
3540
3583
|
}
|
|
3541
3584
|
const field = metadata[fieldIndex];
|
|
@@ -3560,7 +3603,7 @@ function sweepCollection(decoder, coll, seen, allChanges) {
|
|
|
3560
3603
|
seen.add(refId);
|
|
3561
3604
|
// `undefined` = the collection never appeared in the payload at all
|
|
3562
3605
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3563
|
-
// (@
|
|
3606
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3564
3607
|
// means "present with zero entries" → prune everything.
|
|
3565
3608
|
const visited = decoder.resyncVisited.get(refId);
|
|
3566
3609
|
if (visited === undefined) {
|
|
@@ -4245,6 +4288,40 @@ class ArraySchema {
|
|
|
4245
4288
|
// beyond the live range: appends land after the staged tmpItems tail
|
|
4246
4289
|
return tmpItems.length + (index - live);
|
|
4247
4290
|
}
|
|
4291
|
+
/**
|
|
4292
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
4293
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
4294
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
4295
|
+
* at whichever element inherited the slot (issue #231).
|
|
4296
|
+
*
|
|
4297
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
4298
|
+
* the only reader and reaches the index only through a filtered array
|
|
4299
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
4300
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
4301
|
+
* its children every tick.
|
|
4302
|
+
*
|
|
4303
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
4304
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
4305
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
4306
|
+
*/
|
|
4307
|
+
$reindexChildren(from, staged) {
|
|
4308
|
+
if (!this[$changes].hasFilteredFields) {
|
|
4309
|
+
return;
|
|
4310
|
+
} // nothing will read the cache
|
|
4311
|
+
if (typeof this[$childType] === "string") {
|
|
4312
|
+
return;
|
|
4313
|
+
} // primitives have no child tree
|
|
4314
|
+
const tmpItems = this.tmpItems;
|
|
4315
|
+
const length = tmpItems.length;
|
|
4316
|
+
if (staged !== undefined) {
|
|
4317
|
+
while (from < length && tmpItems[from] === staged[from]) {
|
|
4318
|
+
from++;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
for (let i = from; i < length; i++) {
|
|
4322
|
+
tmpItems[i]?.[$changes]?.setParentIndex(this, i);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4248
4325
|
// encoding only. Returns the wire index the change was recorded at
|
|
4249
4326
|
// (undefined when nothing was recorded).
|
|
4250
4327
|
$changeAt(index, value) {
|
|
@@ -4356,6 +4433,7 @@ class ArraySchema {
|
|
|
4356
4433
|
self[$changes].operation(exports.OPERATION.REVERSE);
|
|
4357
4434
|
self.items.reverse();
|
|
4358
4435
|
self.tmpItems.reverse();
|
|
4436
|
+
self.$reindexChildren(0);
|
|
4359
4437
|
return this;
|
|
4360
4438
|
}
|
|
4361
4439
|
/**
|
|
@@ -4407,6 +4485,7 @@ class ArraySchema {
|
|
|
4407
4485
|
// wouldn't OPERATION.MOVE make more sense here?
|
|
4408
4486
|
sortedItems.forEach((_, i) => changeTree.change(i, exports.OPERATION.REPLACE));
|
|
4409
4487
|
self.tmpItems.sort(compareFn);
|
|
4488
|
+
self.$reindexChildren(0);
|
|
4410
4489
|
self.isMovingItems = false;
|
|
4411
4490
|
return this;
|
|
4412
4491
|
}
|
|
@@ -4488,6 +4567,7 @@ class ArraySchema {
|
|
|
4488
4567
|
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
4489
4568
|
}
|
|
4490
4569
|
self.tmpItems.unshift(...items);
|
|
4570
|
+
self.$reindexChildren(items.length); // survivors only — the loop above placed the new items
|
|
4491
4571
|
return self.items.unshift(...items);
|
|
4492
4572
|
}
|
|
4493
4573
|
/**
|
|
@@ -4756,8 +4836,14 @@ class ArraySchema {
|
|
|
4756
4836
|
}
|
|
4757
4837
|
[$onEncodeEnd]() {
|
|
4758
4838
|
const self = this[$proxyTarget] ?? this;
|
|
4839
|
+
const staged = self.tmpItems;
|
|
4759
4840
|
self.tmpItems = self.items.slice();
|
|
4760
|
-
self.deletedIndexes.length
|
|
4841
|
+
if (self.deletedIndexes.length > 0) {
|
|
4842
|
+
// compaction just closed the staged holes — everything above the
|
|
4843
|
+
// lowest one slid down a slot
|
|
4844
|
+
self.$reindexChildren(0, staged);
|
|
4845
|
+
self.deletedIndexes.length = 0;
|
|
4846
|
+
}
|
|
4761
4847
|
}
|
|
4762
4848
|
[$onDecodeEnd]() {
|
|
4763
4849
|
const self = this[$proxyTarget] ?? this;
|
|
@@ -5879,7 +5965,7 @@ registerType("set", { constructor: SetSchema });
|
|
|
5879
5965
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5880
5966
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5881
5967
|
* propagate through the normal reliable channel without consuming the
|
|
5882
|
-
* per-tick budget. Chain `.
|
|
5968
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5883
5969
|
* post-add mutation tracking entirely.
|
|
5884
5970
|
*/
|
|
5885
5971
|
class StreamSchema {
|
|
@@ -6154,12 +6240,11 @@ class FieldBuilder {
|
|
|
6154
6240
|
_default = undefined;
|
|
6155
6241
|
_hasDefault = false;
|
|
6156
6242
|
_view = undefined;
|
|
6157
|
-
_owned = false;
|
|
6158
6243
|
_unreliable = false;
|
|
6159
|
-
|
|
6244
|
+
_patchOnly = false;
|
|
6160
6245
|
_deprecated = false;
|
|
6161
6246
|
_deprecatedThrows = true;
|
|
6162
|
-
|
|
6247
|
+
_fullStateOnly = false;
|
|
6163
6248
|
_stream = false;
|
|
6164
6249
|
_optional = false;
|
|
6165
6250
|
_noSync = false;
|
|
@@ -6194,38 +6279,43 @@ class FieldBuilder {
|
|
|
6194
6279
|
this._view = tag ?? -1;
|
|
6195
6280
|
return this;
|
|
6196
6281
|
}
|
|
6197
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6198
|
-
owned() {
|
|
6199
|
-
this._owned = true;
|
|
6200
|
-
return this;
|
|
6201
|
-
}
|
|
6202
6282
|
/**
|
|
6203
6283
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6204
6284
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6205
|
-
* tagged with `.
|
|
6285
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
6286
|
+
*
|
|
6287
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
6288
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
6289
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
6290
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
6291
|
+
* the field is never written again.
|
|
6206
6292
|
*/
|
|
6207
6293
|
unreliable() {
|
|
6208
6294
|
this._unreliable = true;
|
|
6209
6295
|
return this;
|
|
6210
6296
|
}
|
|
6211
6297
|
/**
|
|
6212
|
-
*
|
|
6213
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6214
|
-
* only after its next mutation is emitted on a
|
|
6215
|
-
* to `.unreliable()`.
|
|
6298
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
6299
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6300
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
6301
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
6216
6302
|
*/
|
|
6217
|
-
|
|
6218
|
-
this.
|
|
6303
|
+
patchOnly() {
|
|
6304
|
+
this._patchOnly = true;
|
|
6219
6305
|
return this;
|
|
6220
6306
|
}
|
|
6221
6307
|
/**
|
|
6222
|
-
*
|
|
6223
|
-
*
|
|
6224
|
-
*
|
|
6225
|
-
*
|
|
6308
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
6309
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
6310
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
6311
|
+
* The mirror of `.patchOnly()`.
|
|
6312
|
+
*
|
|
6313
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
6314
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
6315
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
6226
6316
|
*/
|
|
6227
|
-
|
|
6228
|
-
this.
|
|
6317
|
+
fullStateOnly() {
|
|
6318
|
+
this._fullStateOnly = true;
|
|
6229
6319
|
return this;
|
|
6230
6320
|
}
|
|
6231
6321
|
/**
|
|
@@ -6237,8 +6327,8 @@ class FieldBuilder {
|
|
|
6237
6327
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6238
6328
|
* want on the class for typing convenience without paying any sync cost.
|
|
6239
6329
|
*
|
|
6240
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6241
|
-
* `.unreliable()`, `.
|
|
6330
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6331
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6242
6332
|
* them throws at `schema()` time.
|
|
6243
6333
|
*
|
|
6244
6334
|
* ```ts
|
|
@@ -6280,9 +6370,12 @@ class FieldBuilder {
|
|
|
6280
6370
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6281
6371
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6282
6372
|
*
|
|
6373
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
6374
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
6375
|
+
*
|
|
6283
6376
|
* ```ts
|
|
6284
6377
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6285
|
-
* -
|
|
6378
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6286
6379
|
* )
|
|
6287
6380
|
* ```
|
|
6288
6381
|
*/
|
|
@@ -6317,12 +6410,11 @@ class FieldBuilder {
|
|
|
6317
6410
|
default: this._default,
|
|
6318
6411
|
hasDefault: this._hasDefault,
|
|
6319
6412
|
view: this._view,
|
|
6320
|
-
owned: this._owned,
|
|
6321
6413
|
unreliable: this._unreliable,
|
|
6322
|
-
|
|
6414
|
+
patchOnly: this._patchOnly,
|
|
6323
6415
|
deprecated: this._deprecated,
|
|
6324
6416
|
deprecatedThrows: this._deprecatedThrows,
|
|
6325
|
-
|
|
6417
|
+
fullStateOnly: this._fullStateOnly,
|
|
6326
6418
|
stream: this._stream,
|
|
6327
6419
|
optional: this._optional,
|
|
6328
6420
|
noSync: this._noSync,
|
|
@@ -6576,25 +6668,44 @@ function view(tag = DEFAULT_VIEW_TAG) {
|
|
|
6576
6668
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6577
6669
|
};
|
|
6578
6670
|
}
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6671
|
+
/**
|
|
6672
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
6673
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
6674
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
6675
|
+
*
|
|
6676
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
6677
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
6678
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
6679
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
6680
|
+
* written again.
|
|
6681
|
+
*/
|
|
6583
6682
|
function unreliable(target, field) {
|
|
6584
6683
|
const metadata = Metadata.initialize(target.constructor);
|
|
6585
6684
|
Metadata.setUnreliable(metadata, field);
|
|
6586
6685
|
}
|
|
6587
6686
|
/**
|
|
6588
|
-
* @
|
|
6589
|
-
* encodeAllView).
|
|
6687
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6688
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6590
6689
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6591
6690
|
* the next mutation.
|
|
6592
6691
|
*
|
|
6593
6692
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6594
6693
|
*/
|
|
6595
|
-
function
|
|
6694
|
+
function patchOnly(target, field) {
|
|
6695
|
+
const metadata = Metadata.initialize(target.constructor);
|
|
6696
|
+
Metadata.setPatchOnly(metadata, field);
|
|
6697
|
+
}
|
|
6698
|
+
/**
|
|
6699
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
6700
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
6701
|
+
* client has joined are not propagated to it — populate these fields
|
|
6702
|
+
* before clients connect (e.g. during onCreate).
|
|
6703
|
+
*
|
|
6704
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
6705
|
+
*/
|
|
6706
|
+
function fullStateOnly(target, field) {
|
|
6596
6707
|
const metadata = Metadata.initialize(target.constructor);
|
|
6597
|
-
Metadata.
|
|
6708
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6598
6709
|
}
|
|
6599
6710
|
function type(type, options) {
|
|
6600
6711
|
return function (target, field) {
|
|
@@ -6966,11 +7077,10 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6966
7077
|
}
|
|
6967
7078
|
};
|
|
6968
7079
|
const viewTagFields = {};
|
|
6969
|
-
const ownedFields = [];
|
|
6970
7080
|
const unreliableFields = [];
|
|
6971
|
-
const
|
|
7081
|
+
const patchOnlyFields = [];
|
|
6972
7082
|
const deprecatedFields = {};
|
|
6973
|
-
const
|
|
7083
|
+
const fullStateOnlyFields = [];
|
|
6974
7084
|
const streamFields = [];
|
|
6975
7085
|
const streamPriorityFields = {};
|
|
6976
7086
|
const optionalFields = [];
|
|
@@ -6982,15 +7092,22 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6982
7092
|
// Local-only field: skip metadata registration entirely so it is
|
|
6983
7093
|
// never encoded/decoded, but still seed its construction default
|
|
6984
7094
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6985
|
-
if (def.view !== undefined || def.
|
|
6986
|
-
def.
|
|
7095
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7096
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6987
7097
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6988
|
-
`together with a sync-only modifier (.view/.
|
|
7098
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6989
7099
|
`A local-only field cannot be synchronized.`);
|
|
6990
7100
|
}
|
|
6991
7101
|
seedDefault(fieldName, def);
|
|
6992
7102
|
continue;
|
|
6993
7103
|
}
|
|
7104
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
7105
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
7106
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
7107
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
7108
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
7109
|
+
`so the field would never reach a client — use .noSync() if that is intended.`);
|
|
7110
|
+
}
|
|
6994
7111
|
const normalizedType = getNormalizedType(def.type);
|
|
6995
7112
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
6996
7113
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -7001,20 +7118,17 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
7001
7118
|
if (def.view !== undefined) {
|
|
7002
7119
|
viewTagFields[fieldName] = def.view;
|
|
7003
7120
|
}
|
|
7004
|
-
if (def.owned) {
|
|
7005
|
-
ownedFields.push(fieldName);
|
|
7006
|
-
}
|
|
7007
7121
|
if (def.unreliable) {
|
|
7008
7122
|
unreliableFields.push(fieldName);
|
|
7009
7123
|
}
|
|
7010
|
-
if (def.
|
|
7011
|
-
|
|
7124
|
+
if (def.patchOnly) {
|
|
7125
|
+
patchOnlyFields.push(fieldName);
|
|
7012
7126
|
}
|
|
7013
7127
|
if (def.deprecated) {
|
|
7014
7128
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7015
7129
|
}
|
|
7016
|
-
if (def.
|
|
7017
|
-
|
|
7130
|
+
if (def.fullStateOnly) {
|
|
7131
|
+
fullStateOnlyFields.push(fieldName);
|
|
7018
7132
|
}
|
|
7019
7133
|
if (def.stream) {
|
|
7020
7134
|
streamFields.push(fieldName);
|
|
@@ -7097,22 +7211,19 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
7097
7211
|
for (const fieldName in viewTagFields) {
|
|
7098
7212
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7099
7213
|
}
|
|
7100
|
-
for (const fieldName of ownedFields) {
|
|
7101
|
-
owned(klass.prototype, fieldName);
|
|
7102
|
-
}
|
|
7103
7214
|
for (const fieldName of unreliableFields) {
|
|
7104
7215
|
unreliable(klass.prototype, fieldName);
|
|
7105
7216
|
}
|
|
7106
|
-
for (const fieldName of
|
|
7107
|
-
|
|
7217
|
+
for (const fieldName of patchOnlyFields) {
|
|
7218
|
+
patchOnly(klass.prototype, fieldName);
|
|
7108
7219
|
}
|
|
7109
7220
|
for (const fieldName in deprecatedFields) {
|
|
7110
7221
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7111
7222
|
}
|
|
7112
|
-
if (
|
|
7223
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7113
7224
|
const metadata = klass[Symbol.metadata];
|
|
7114
|
-
for (const fieldName of
|
|
7115
|
-
Metadata.
|
|
7225
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7226
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7116
7227
|
}
|
|
7117
7228
|
for (const fieldName of streamFields) {
|
|
7118
7229
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7761,7 +7872,7 @@ class Root {
|
|
|
7761
7872
|
const previousRefCount = this.refCount[refId];
|
|
7762
7873
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7763
7874
|
//
|
|
7764
|
-
// Re-stage every currently-populated non-
|
|
7875
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7765
7876
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7766
7877
|
// re-emits it on the correct channel. Two triggers:
|
|
7767
7878
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7851,14 +7962,10 @@ class Root {
|
|
|
7851
7962
|
const parentNode = parent[$changes][nodeField];
|
|
7852
7963
|
if (!parentNode || parentNode === node)
|
|
7853
7964
|
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
|
|
7965
|
+
// Positions are strictly increasing along the list, so this is an
|
|
7966
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
7967
|
+
if (node.position > parentNode.position)
|
|
7968
|
+
return;
|
|
7862
7969
|
// Remove node from current position
|
|
7863
7970
|
if (node.prev) {
|
|
7864
7971
|
node.prev.next = node.next;
|
|
@@ -7872,16 +7979,18 @@ class Root {
|
|
|
7872
7979
|
else {
|
|
7873
7980
|
changeSet.tail = node.prev;
|
|
7874
7981
|
}
|
|
7875
|
-
//
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7982
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
7983
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
7984
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
7985
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
7986
|
+
// a fresh max position, keeping the invariant append-only.
|
|
7987
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
7988
|
+
// subtree re-serializes parent-first behind it.)
|
|
7989
|
+
node.prev = changeSet.tail;
|
|
7990
|
+
node.next = undefined;
|
|
7991
|
+
changeSet.tail.next = node; // parentNode remains in the list — never empty here
|
|
7992
|
+
changeSet.tail = node;
|
|
7993
|
+
node.position = changeSet.nextPosition++;
|
|
7885
7994
|
}
|
|
7886
7995
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7887
7996
|
if (existingNode) {
|
|
@@ -7903,12 +8012,12 @@ class Root {
|
|
|
7903
8012
|
node.changeTree = changeTree;
|
|
7904
8013
|
node.next = undefined;
|
|
7905
8014
|
node.prev = undefined;
|
|
7906
|
-
node.position = 0;
|
|
7907
8015
|
}
|
|
7908
8016
|
else {
|
|
7909
8017
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7910
8018
|
}
|
|
7911
8019
|
if (!list.next) {
|
|
8020
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7912
8021
|
list.next = node;
|
|
7913
8022
|
list.tail = node;
|
|
7914
8023
|
}
|
|
@@ -7917,6 +8026,7 @@ class Root {
|
|
|
7917
8026
|
list.tail.next = node;
|
|
7918
8027
|
list.tail = node;
|
|
7919
8028
|
}
|
|
8029
|
+
node.position = list.nextPosition++;
|
|
7920
8030
|
return node;
|
|
7921
8031
|
}
|
|
7922
8032
|
/**
|
|
@@ -8015,6 +8125,7 @@ function _fullSyncWalk(ctx, changeTree) {
|
|
|
8015
8125
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8016
8126
|
ctx.isSchema = desc.isSchema;
|
|
8017
8127
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8128
|
+
ctx.tags = desc.tags;
|
|
8018
8129
|
ctx.structSwitchEmitted = false;
|
|
8019
8130
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8020
8131
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8054,10 +8165,13 @@ function encodeChangeCb(ctx, fieldIndex, op) {
|
|
|
8054
8165
|
}
|
|
8055
8166
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8056
8167
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8057
|
-
// itself carries a @view tag.
|
|
8058
|
-
//
|
|
8168
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
8169
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
8170
|
+
// that arm needs a Schema with more than 32 fields.
|
|
8059
8171
|
const fieldFiltered = ctx.isSchema
|
|
8060
|
-
? (ctx.treeIsFiltered || (
|
|
8172
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8173
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8174
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8061
8175
|
: ctx.treeIsFiltered;
|
|
8062
8176
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8063
8177
|
return;
|
|
@@ -8112,7 +8226,7 @@ class Encoder {
|
|
|
8112
8226
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8113
8227
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8114
8228
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8115
|
-
filterBitmask: 0,
|
|
8229
|
+
filterBitmask: 0, tags: undefined,
|
|
8116
8230
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8117
8231
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8118
8232
|
};
|
|
@@ -8169,6 +8283,7 @@ class Encoder {
|
|
|
8169
8283
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8170
8284
|
ctx.isSchema = desc.isSchema;
|
|
8171
8285
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8286
|
+
ctx.tags = desc.tags;
|
|
8172
8287
|
ctx.structSwitchEmitted = false;
|
|
8173
8288
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8174
8289
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8420,7 +8535,7 @@ class Encoder {
|
|
|
8420
8535
|
// Emit each element's full state — forEachLive walks populated
|
|
8421
8536
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8422
8537
|
// Covers both static elements (dirty state was reset by
|
|
8423
|
-
// inheritedFlags'
|
|
8538
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8424
8539
|
// has dirty state but the main loop skipped them because
|
|
8425
8540
|
// they're filtered).
|
|
8426
8541
|
for (const element of emittedElements) {
|
|
@@ -8499,20 +8614,81 @@ class Encoder {
|
|
|
8499
8614
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8500
8615
|
// into `_stream.priority` when the stream was attached. Users
|
|
8501
8616
|
// can also override per-instance by assigning to the setter.
|
|
8617
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
8618
|
+
// wins over the declaration-scope one: it closes over the
|
|
8619
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
8620
|
+
const perView = st.priorityByView?.get(viewId);
|
|
8621
|
+
const usePerView = perView !== undefined;
|
|
8502
8622
|
const priority = st.priority;
|
|
8503
|
-
|
|
8504
|
-
//
|
|
8505
|
-
//
|
|
8623
|
+
const max = st.maxPerTick;
|
|
8624
|
+
// Select the `max` highest-priority candidates.
|
|
8625
|
+
//
|
|
8626
|
+
// A comparator-based sort invokes the callback twice per
|
|
8627
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
8628
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
8629
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
8630
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
8631
|
+
// the scratch by `max` rather than by the backlog.
|
|
8632
|
+
//
|
|
8633
|
+
// Ties keep the earlier position (both comparisons below are
|
|
8634
|
+
// strict), so equal-priority entries still drain in insertion
|
|
8635
|
+
// order.
|
|
8506
8636
|
const positions = [];
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8637
|
+
const stale = [];
|
|
8638
|
+
if (usePerView || priority !== undefined) {
|
|
8639
|
+
const bestPos = [];
|
|
8640
|
+
const bestScore = [];
|
|
8641
|
+
let filled = 0;
|
|
8642
|
+
for (const pos of pending) {
|
|
8643
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
8644
|
+
// through the same lookup regardless of $items layout.
|
|
8645
|
+
const element = s[$getByIndex](pos);
|
|
8646
|
+
if (element === undefined) {
|
|
8647
|
+
// Removed after being queued — drop it below without
|
|
8648
|
+
// spending budget on it.
|
|
8649
|
+
stale.push(pos);
|
|
8650
|
+
continue;
|
|
8651
|
+
}
|
|
8652
|
+
const score = usePerView
|
|
8653
|
+
? perView(element)
|
|
8654
|
+
: priority(view, element);
|
|
8655
|
+
// Window not yet full: always insert.
|
|
8656
|
+
if (filled < max) {
|
|
8657
|
+
let j = filled++;
|
|
8658
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8659
|
+
bestScore[j] = bestScore[j - 1];
|
|
8660
|
+
bestPos[j] = bestPos[j - 1];
|
|
8661
|
+
j--;
|
|
8662
|
+
}
|
|
8663
|
+
bestScore[j] = score;
|
|
8664
|
+
bestPos[j] = pos;
|
|
8665
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
8666
|
+
}
|
|
8667
|
+
else if (score > bestScore[max - 1]) {
|
|
8668
|
+
let j = max - 1;
|
|
8669
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8670
|
+
bestScore[j] = bestScore[j - 1];
|
|
8671
|
+
bestPos[j] = bestPos[j - 1];
|
|
8672
|
+
j--;
|
|
8673
|
+
}
|
|
8674
|
+
bestScore[j] = score;
|
|
8675
|
+
bestPos[j] = pos;
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
for (let i = 0; i < filled; i++)
|
|
8679
|
+
positions.push(bestPos[i]);
|
|
8513
8680
|
}
|
|
8514
|
-
|
|
8515
|
-
|
|
8681
|
+
else {
|
|
8682
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
8683
|
+
for (const pos of pending) {
|
|
8684
|
+
if (positions.length >= max)
|
|
8685
|
+
break;
|
|
8686
|
+
positions.push(pos);
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
for (const pos of stale)
|
|
8690
|
+
pending.delete(pos);
|
|
8691
|
+
const count = positions.length;
|
|
8516
8692
|
let sent = st.sentByView.get(viewId);
|
|
8517
8693
|
if (sent === undefined) {
|
|
8518
8694
|
sent = new Set();
|
|
@@ -9867,6 +10043,31 @@ const _disposeRegistry = new FinalizationRegistry(({ root, id, slot, bit }) => {
|
|
|
9867
10043
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9868
10044
|
root.releaseViewId(id);
|
|
9869
10045
|
});
|
|
10046
|
+
/**
|
|
10047
|
+
* Compact description of a rejected argument, for warning messages.
|
|
10048
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
10049
|
+
* populated collection inspects into dozens of lines of encoder
|
|
10050
|
+
* internals and buries the message that matters.
|
|
10051
|
+
*/
|
|
10052
|
+
function describeArg(value) {
|
|
10053
|
+
if (value === undefined) {
|
|
10054
|
+
return "undefined";
|
|
10055
|
+
}
|
|
10056
|
+
if (value === null) {
|
|
10057
|
+
return "null";
|
|
10058
|
+
}
|
|
10059
|
+
const type = typeof value;
|
|
10060
|
+
if (type === "string") {
|
|
10061
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
10062
|
+
}
|
|
10063
|
+
if (type !== "object" && type !== "function") {
|
|
10064
|
+
return `${type} ${String(value)}`;
|
|
10065
|
+
}
|
|
10066
|
+
if (Array.isArray(value)) {
|
|
10067
|
+
return `Array(${value.length})`;
|
|
10068
|
+
}
|
|
10069
|
+
return value.constructor?.name ?? "Object";
|
|
10070
|
+
}
|
|
9870
10071
|
class StateView {
|
|
9871
10072
|
iterable;
|
|
9872
10073
|
/**
|
|
@@ -10091,12 +10292,12 @@ class StateView {
|
|
|
10091
10292
|
}
|
|
10092
10293
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10093
10294
|
const changeTree = obj?.[$changes];
|
|
10094
|
-
const parentChangeTree = changeTree.parent;
|
|
10095
10295
|
if (!changeTree) {
|
|
10096
|
-
console.warn(
|
|
10296
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10097
10297
|
return false;
|
|
10098
10298
|
}
|
|
10099
|
-
|
|
10299
|
+
const parentChangeTree = changeTree.parent;
|
|
10300
|
+
if (!parentChangeTree &&
|
|
10100
10301
|
obj[$refId] !== 0 // allow root object
|
|
10101
10302
|
) {
|
|
10102
10303
|
/**
|
|
@@ -10377,9 +10578,9 @@ class StateView {
|
|
|
10377
10578
|
}
|
|
10378
10579
|
}
|
|
10379
10580
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10380
|
-
const changeTree = obj[$changes];
|
|
10581
|
+
const changeTree = obj?.[$changes];
|
|
10381
10582
|
if (!changeTree) {
|
|
10382
|
-
console.warn(
|
|
10583
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10383
10584
|
return this;
|
|
10384
10585
|
}
|
|
10385
10586
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10517,32 +10718,47 @@ class StateView {
|
|
|
10517
10718
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10518
10719
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10519
10720
|
}
|
|
10520
|
-
|
|
10521
|
-
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
10522
|
-
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
10523
|
-
* future content changes — every subsequent push / set / add to the
|
|
10524
|
-
* collection automatically flows to this view, and every removal
|
|
10525
|
-
* queues a DELETE op. Works on every collection type:
|
|
10526
|
-
*
|
|
10527
|
-
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
10528
|
-
* new children are force-shipped immediately (equivalent to
|
|
10529
|
-
* `view.add(child)` per item).
|
|
10530
|
-
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
10531
|
-
* enqueued into `_pendingByView` so the priority pass drains them
|
|
10532
|
-
* respecting `maxPerTick`.
|
|
10533
|
-
*
|
|
10534
|
-
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
10535
|
-
* collection is a no-op.
|
|
10536
|
-
*/
|
|
10537
|
-
subscribe(collection) {
|
|
10721
|
+
subscribe(collection, priority) {
|
|
10538
10722
|
const tree = collection?.[$changes];
|
|
10539
10723
|
if (!tree) {
|
|
10540
|
-
console.warn(
|
|
10724
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10541
10725
|
return this;
|
|
10542
10726
|
}
|
|
10543
10727
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10544
10728
|
this._bindRoot(tree.root);
|
|
10545
10729
|
}
|
|
10730
|
+
if (priority !== undefined) {
|
|
10731
|
+
if (!tree.isStreamCollection) {
|
|
10732
|
+
// Name the field rather than dumping the collection — a
|
|
10733
|
+
// populated MapSchema inspects into dozens of lines of
|
|
10734
|
+
// internals and buries the message.
|
|
10735
|
+
const kind = collection?.constructor?.name ?? "collection";
|
|
10736
|
+
const parent = tree.parent;
|
|
10737
|
+
if (parent === undefined) {
|
|
10738
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
10739
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
10740
|
+
`Subscribe after assigning it to the state.`);
|
|
10741
|
+
}
|
|
10742
|
+
else {
|
|
10743
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
10744
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
10745
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
10746
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
10747
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`);
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
else {
|
|
10751
|
+
// Set before the idempotency return below, so re-subscribing
|
|
10752
|
+
// is the documented way to retarget this view's ordering.
|
|
10753
|
+
const st = ensureStreamState(collection);
|
|
10754
|
+
if (priority === null) {
|
|
10755
|
+
st.priorityByView?.delete(this.id);
|
|
10756
|
+
}
|
|
10757
|
+
else {
|
|
10758
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
}
|
|
10546
10762
|
if (this.isSubscribed(tree))
|
|
10547
10763
|
return this;
|
|
10548
10764
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10588,7 +10804,7 @@ class StateView {
|
|
|
10588
10804
|
unsubscribe(collection) {
|
|
10589
10805
|
const tree = collection?.[$changes];
|
|
10590
10806
|
if (!tree) {
|
|
10591
|
-
console.warn(
|
|
10807
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10592
10808
|
return this;
|
|
10593
10809
|
}
|
|
10594
10810
|
if (!this.isSubscribed(tree))
|
|
@@ -10717,15 +10933,15 @@ exports.encodeKeyValueOperation = encodeKeyValueOperation;
|
|
|
10717
10933
|
exports.encodeMapEntry = encodeMapEntry;
|
|
10718
10934
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
10719
10935
|
exports.entity = entity;
|
|
10936
|
+
exports.fullStateOnly = fullStateOnly;
|
|
10720
10937
|
exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
|
|
10721
10938
|
exports.getEncodeDescriptor = getEncodeDescriptor;
|
|
10722
10939
|
exports.getRawChangesCallback = getRawChangesCallback;
|
|
10723
10940
|
exports.isBuilder = isBuilder;
|
|
10724
|
-
exports.
|
|
10941
|
+
exports.patchOnly = patchOnly;
|
|
10725
10942
|
exports.registerType = registerType;
|
|
10726
10943
|
exports.schema = schema;
|
|
10727
10944
|
exports.t = t;
|
|
10728
|
-
exports.transient = transient;
|
|
10729
10945
|
exports.type = type;
|
|
10730
10946
|
exports.unreliable = unreliable;
|
|
10731
10947
|
exports.view = view;
|