@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.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.schema = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
const SWITCH_TO_STRUCTURE = 255; //
|
|
7
|
+
const SWITCH_TO_STRUCTURE = 255; // same byte as `DELETE_AND_ADD | 63`, which is why field index 63 is unassignable (Metadata.MAX_FIELDS)
|
|
8
8
|
const TYPE_ID = 213;
|
|
9
9
|
/**
|
|
10
10
|
* Encoding Schema field operations.
|
|
@@ -124,16 +124,10 @@
|
|
|
124
124
|
* Metadata
|
|
125
125
|
*/
|
|
126
126
|
const $descriptors = "~descriptors";
|
|
127
|
-
/**
|
|
128
|
-
* Per-class bitmask: bit i set iff field i carries a @view tag.
|
|
129
|
-
* Lazily computed from $viewFieldIndexes on first encode pass.
|
|
130
|
-
* Skips the per-field metadata[i].tag property chase in the hot encode loop.
|
|
131
|
-
*/
|
|
132
|
-
const $filterBitmask = "~__filterBitmask";
|
|
133
127
|
/**
|
|
134
128
|
* Cached per-class encode descriptor: bundles encoder fn, filter fn,
|
|
135
|
-
* metadata, isSchema flag
|
|
136
|
-
* the constructor. Replaces
|
|
129
|
+
* metadata, isSchema flag and the per-field arrays into one object stashed
|
|
130
|
+
* on the constructor. Replaces several separate per-tree property chases /
|
|
137
131
|
* function calls in the encode loop with a single property load.
|
|
138
132
|
*/
|
|
139
133
|
const $encodeDescriptor = "~__encodeDescriptor";
|
|
@@ -143,8 +137,8 @@
|
|
|
143
137
|
const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
144
138
|
const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
145
139
|
const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
146
|
-
const $
|
|
147
|
-
const $
|
|
140
|
+
const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
141
|
+
const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
148
142
|
const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
149
143
|
const $streamPriorities = "~__streamPriorities";
|
|
150
144
|
|
|
@@ -825,6 +819,7 @@
|
|
|
825
819
|
return;
|
|
826
820
|
st.pendingByView.delete(viewId);
|
|
827
821
|
st.sentByView.delete(viewId);
|
|
822
|
+
st.priorityByView?.delete(viewId);
|
|
828
823
|
}
|
|
829
824
|
|
|
830
825
|
const WIRE_BY_BITS = {
|
|
@@ -1070,6 +1065,15 @@
|
|
|
1070
1065
|
}
|
|
1071
1066
|
}
|
|
1072
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Field indexes ride in the low 6 bits of the operation byte
|
|
1070
|
+
* (`(index | operation) & 255`), which leaves room for 0..63. Index 63 is
|
|
1071
|
+
* given up: `DELETE_AND_ADD | 63` is 255, the same byte the decoder claims
|
|
1072
|
+
* as SWITCH_TO_STRUCTURE before any field decoder sees it. Every nullable
|
|
1073
|
+
* field can produce that operation (delete-then-set in one tick merges to
|
|
1074
|
+
* DELETE_AND_ADD), so the slot is unusable rather than partly usable.
|
|
1075
|
+
*/
|
|
1076
|
+
const MAX_FIELDS = 63;
|
|
1073
1077
|
/**
|
|
1074
1078
|
* Given a normalized field type (`"number"`, `{ map: Foo }`, `Player`,
|
|
1075
1079
|
* etc.), split into the collection-type descriptor (`{ constructor:
|
|
@@ -1133,10 +1137,12 @@
|
|
|
1133
1137
|
}
|
|
1134
1138
|
const Metadata = {
|
|
1135
1139
|
addField(metadata, index, name, type, descriptor) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1140
|
+
// `index` is 0-based, so 62 is the last usable slot — see MAX_FIELDS
|
|
1141
|
+
// for why 63 is off limits.
|
|
1142
|
+
if (index >= MAX_FIELDS) {
|
|
1143
|
+
throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to ${MAX_FIELDS} fields.`);
|
|
1138
1144
|
}
|
|
1139
|
-
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@
|
|
1145
|
+
metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@deprecated / @unreliable)
|
|
1140
1146
|
{
|
|
1141
1147
|
type: getNormalizedType(type),
|
|
1142
1148
|
index,
|
|
@@ -1286,31 +1292,43 @@
|
|
|
1286
1292
|
}
|
|
1287
1293
|
metadata[$unreliableFieldIndexes].push(index);
|
|
1288
1294
|
},
|
|
1289
|
-
|
|
1295
|
+
setPatchOnly(metadata, fieldName) {
|
|
1290
1296
|
const index = metadata[fieldName];
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1297
|
+
// patchOnly + fullStateOnly are the only two delivery channels —
|
|
1298
|
+
// excluding a field from both would silently never reach a client.
|
|
1299
|
+
// (The builder validates earlier; this guards the decorator path.)
|
|
1300
|
+
if (metadata[index].fullStateOnly) {
|
|
1301
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1302
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1303
|
+
}
|
|
1304
|
+
metadata[index].patchOnly = true;
|
|
1305
|
+
if (!metadata[$patchOnlyFieldIndexes]) {
|
|
1306
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1294
1307
|
value: [],
|
|
1295
1308
|
enumerable: false,
|
|
1296
1309
|
configurable: true,
|
|
1297
1310
|
writable: true,
|
|
1298
1311
|
});
|
|
1299
1312
|
}
|
|
1300
|
-
metadata[$
|
|
1313
|
+
metadata[$patchOnlyFieldIndexes].push(index);
|
|
1301
1314
|
},
|
|
1302
|
-
|
|
1315
|
+
setFullStateOnly(metadata, fieldName) {
|
|
1303
1316
|
const index = metadata[fieldName];
|
|
1304
|
-
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1317
|
+
// Mirror of the guard in setPatchOnly — covers both decorator orders.
|
|
1318
|
+
if (metadata[index].patchOnly) {
|
|
1319
|
+
throw new Error(`field "${fieldName}" cannot be both patchOnly and fullStateOnly — ` +
|
|
1320
|
+
`those are the only two delivery channels, so the field would never reach a client.`);
|
|
1321
|
+
}
|
|
1322
|
+
metadata[index].fullStateOnly = true;
|
|
1323
|
+
if (!metadata[$fullStateOnlyFieldIndexes]) {
|
|
1324
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1307
1325
|
value: [],
|
|
1308
1326
|
enumerable: false,
|
|
1309
1327
|
configurable: true,
|
|
1310
1328
|
writable: true,
|
|
1311
1329
|
});
|
|
1312
1330
|
}
|
|
1313
|
-
metadata[$
|
|
1331
|
+
metadata[$fullStateOnlyFieldIndexes].push(index);
|
|
1314
1332
|
},
|
|
1315
1333
|
setStream(metadata, fieldName) {
|
|
1316
1334
|
const index = metadata[fieldName];
|
|
@@ -1474,19 +1492,19 @@
|
|
|
1474
1492
|
writable: true,
|
|
1475
1493
|
});
|
|
1476
1494
|
}
|
|
1477
|
-
// $
|
|
1478
|
-
if (parentMetadata[$
|
|
1479
|
-
Object.defineProperty(metadata, $
|
|
1480
|
-
value: [...parentMetadata[$
|
|
1495
|
+
// $patchOnlyFieldIndexes
|
|
1496
|
+
if (parentMetadata[$patchOnlyFieldIndexes] !== undefined) {
|
|
1497
|
+
Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
|
|
1498
|
+
value: [...parentMetadata[$patchOnlyFieldIndexes]],
|
|
1481
1499
|
enumerable: false,
|
|
1482
1500
|
configurable: true,
|
|
1483
1501
|
writable: true,
|
|
1484
1502
|
});
|
|
1485
1503
|
}
|
|
1486
|
-
// $
|
|
1487
|
-
if (parentMetadata[$
|
|
1488
|
-
Object.defineProperty(metadata, $
|
|
1489
|
-
value: [...parentMetadata[$
|
|
1504
|
+
// $fullStateOnlyFieldIndexes
|
|
1505
|
+
if (parentMetadata[$fullStateOnlyFieldIndexes] !== undefined) {
|
|
1506
|
+
Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
|
|
1507
|
+
value: [...parentMetadata[$fullStateOnlyFieldIndexes]],
|
|
1490
1508
|
enumerable: false,
|
|
1491
1509
|
configurable: true,
|
|
1492
1510
|
writable: true,
|
|
@@ -1544,11 +1562,11 @@
|
|
|
1544
1562
|
hasUnreliableAtIndex(metadata, index) {
|
|
1545
1563
|
return metadata?.[$unreliableFieldIndexes]?.includes(index);
|
|
1546
1564
|
},
|
|
1547
|
-
|
|
1548
|
-
return metadata?.[$
|
|
1565
|
+
hasPatchOnlyAtIndex(metadata, index) {
|
|
1566
|
+
return metadata?.[$patchOnlyFieldIndexes]?.includes(index);
|
|
1549
1567
|
},
|
|
1550
|
-
|
|
1551
|
-
return metadata?.[$
|
|
1568
|
+
hasFullStateOnlyAtIndex(metadata, index) {
|
|
1569
|
+
return metadata?.[$fullStateOnlyFieldIndexes]?.includes(index);
|
|
1552
1570
|
},
|
|
1553
1571
|
hasStreamAtIndex(metadata, index) {
|
|
1554
1572
|
return metadata?.[$streamFieldIndexes]?.includes(index);
|
|
@@ -1559,7 +1577,7 @@
|
|
|
1559
1577
|
// by passing the user's callback as ctx. No per-call allocation.
|
|
1560
1578
|
const _invokeNoCtx$2 = (cb, index, op) => cb(index, op);
|
|
1561
1579
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1562
|
-
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤
|
|
1580
|
+
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤63 fields)
|
|
1563
1581
|
// ──────────────────────────────────────────────────────────────────────────
|
|
1564
1582
|
/**
|
|
1565
1583
|
* Schema field operations are limited to ADD(128), DELETE(64), and
|
|
@@ -1752,37 +1770,15 @@
|
|
|
1752
1770
|
* ctor[$filter]
|
|
1753
1771
|
* ctor[Symbol.metadata]
|
|
1754
1772
|
* Metadata.isValidInstance(ref)
|
|
1755
|
-
* getFilterBitmask(metadata)
|
|
1756
1773
|
*
|
|
1757
1774
|
* Lives in its own file to break the Encoder.ts ↔ ChangeTree.ts import
|
|
1758
1775
|
* cycle (ChangeTree caches descriptors at construction; Encoder reads them
|
|
1759
1776
|
* during encode).
|
|
1760
1777
|
*/
|
|
1761
|
-
function computeFilterBitmask(metadata) {
|
|
1762
|
-
if (metadata === undefined)
|
|
1763
|
-
return 0;
|
|
1764
|
-
let bm = metadata[$filterBitmask];
|
|
1765
|
-
if (bm !== undefined)
|
|
1766
|
-
return bm;
|
|
1767
|
-
bm = 0;
|
|
1768
|
-
const tagged = metadata[$viewFieldIndexes];
|
|
1769
|
-
if (tagged !== undefined) {
|
|
1770
|
-
for (let i = 0, len = tagged.length; i < len; i++)
|
|
1771
|
-
bm |= (1 << tagged[i]);
|
|
1772
|
-
}
|
|
1773
|
-
// Non-enumerable so `for (const k in metadata)` iteration in TypeContext
|
|
1774
|
-
// and elsewhere doesn't mistake this cache for a real field index.
|
|
1775
|
-
Object.defineProperty(metadata, $filterBitmask, {
|
|
1776
|
-
value: bm,
|
|
1777
|
-
enumerable: false,
|
|
1778
|
-
writable: true,
|
|
1779
|
-
configurable: true,
|
|
1780
|
-
});
|
|
1781
|
-
return bm;
|
|
1782
|
-
}
|
|
1783
1778
|
/**
|
|
1784
1779
|
* Bitmask of field indexes 0–31 in `indexes`. For fields ≥32 callers must
|
|
1785
|
-
* fall back to the array lookup
|
|
1780
|
+
* fall back to the array lookup — shift counts wrap at 32, so an unguarded
|
|
1781
|
+
* `1 << 40` would set bit 8 and misclassify field 8.
|
|
1786
1782
|
*/
|
|
1787
1783
|
function indexesToBitmask(indexes) {
|
|
1788
1784
|
if (indexes === undefined)
|
|
@@ -1858,12 +1854,12 @@
|
|
|
1858
1854
|
filter,
|
|
1859
1855
|
metadata,
|
|
1860
1856
|
isSchema,
|
|
1861
|
-
filterBitmask: isSchema ?
|
|
1862
|
-
|
|
1857
|
+
filterBitmask: isSchema ? indexesToBitmask(metadata?.[$viewFieldIndexes]) : 0,
|
|
1858
|
+
hasAnyFullStateOnly: (metadata?.[$fullStateOnlyFieldIndexes]?.length ?? 0) > 0,
|
|
1863
1859
|
hasAnyUnreliable: (metadata?.[$unreliableFieldIndexes]?.length ?? 0) > 0,
|
|
1864
1860
|
hasAnyStream: (metadata?.[$streamFieldIndexes]?.length ?? 0) > 0,
|
|
1865
1861
|
hasAnyView,
|
|
1866
|
-
|
|
1862
|
+
fullStateOnlyBitmask: indexesToBitmask(metadata?.[$fullStateOnlyFieldIndexes]),
|
|
1867
1863
|
unreliableBitmask: indexesToBitmask(metadata?.[$unreliableFieldIndexes]),
|
|
1868
1864
|
streamBitmask: indexesToBitmask(metadata?.[$streamFieldIndexes]),
|
|
1869
1865
|
names: arrays.names,
|
|
@@ -1922,6 +1918,34 @@
|
|
|
1922
1918
|
tree._parentIndex = index;
|
|
1923
1919
|
}
|
|
1924
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Move `parent`'s existing chain entry to `index`, skipping the attachment
|
|
1923
|
+
* work `addParent` does. `parent` must already be a parent of `tree`.
|
|
1924
|
+
*
|
|
1925
|
+
* Called by collections whose wire slots shift (ArraySchema): StateView
|
|
1926
|
+
* addresses per-view ADD/DELETE by that index, so it has to follow the
|
|
1927
|
+
* element it names.
|
|
1928
|
+
*/
|
|
1929
|
+
function setParentIndex(tree, parent, index) {
|
|
1930
|
+
if (tree.extraParents === undefined) {
|
|
1931
|
+
tree._parentIndex = index; // sole parent, so it is `parent`
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
// Shared instance — move only the entry `parent` owns. Matching goes
|
|
1935
|
+
// through `$changes` because ArraySchema arrives proxied (see removeParent
|
|
1936
|
+
// below), and `extraParents` only ever fills by demoting `parentRef`, so
|
|
1937
|
+
// the inline parent is set here.
|
|
1938
|
+
if (tree.parentRef[$changes] === parent[$changes]) {
|
|
1939
|
+
tree._parentIndex = index;
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
1943
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
1944
|
+
entry.index = index;
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1925
1949
|
/**
|
|
1926
1950
|
* Remove a parent from the chain.
|
|
1927
1951
|
* @returns true if parent was found and removed (Root.remove relies on this).
|
|
@@ -1999,13 +2023,13 @@
|
|
|
1999
2023
|
}
|
|
2000
2024
|
|
|
2001
2025
|
/**
|
|
2002
|
-
* Walk all currently-populated non-
|
|
2026
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
2003
2027
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
2004
2028
|
* StateView.add to derive full-sync output from the live structure.
|
|
2005
2029
|
*
|
|
2006
|
-
*
|
|
2030
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
2007
2031
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
2008
|
-
* field is @
|
|
2032
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
2009
2033
|
*/
|
|
2010
2034
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
2011
2035
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
@@ -2019,10 +2043,10 @@
|
|
|
2019
2043
|
// types. See `ChangeTree.refTarget` doc.
|
|
2020
2044
|
const ref = tree.refTarget;
|
|
2021
2045
|
if (ref[$childType] !== undefined) {
|
|
2022
|
-
// Collection inheriting @
|
|
2046
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
2023
2047
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
2024
2048
|
// absent from full-sync output is never pruned client-side.
|
|
2025
|
-
if (tree.
|
|
2049
|
+
if (tree.isPatchOnly)
|
|
2026
2050
|
return;
|
|
2027
2051
|
// Collection types: dispatch by shape.
|
|
2028
2052
|
if (Array.isArray(ref.items)) {
|
|
@@ -2051,7 +2075,7 @@
|
|
|
2051
2075
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
2052
2076
|
// the setter records a DELETE when a field is set to null or
|
|
2053
2077
|
// undefined, so it should not appear in full-sync output.
|
|
2054
|
-
// (@
|
|
2078
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
2055
2079
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
2056
2080
|
//
|
|
2057
2081
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -2061,13 +2085,13 @@
|
|
|
2061
2085
|
if (!metadata)
|
|
2062
2086
|
return;
|
|
2063
2087
|
const numFields = (metadata[$numFields] ?? -1);
|
|
2064
|
-
const
|
|
2088
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
2065
2089
|
const names = tree.encDescriptor.names;
|
|
2066
2090
|
for (let i = 0; i <= numFields; i++) {
|
|
2067
2091
|
const name = names[i];
|
|
2068
2092
|
if (name === undefined)
|
|
2069
2093
|
continue;
|
|
2070
|
-
if (
|
|
2094
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i))
|
|
2071
2095
|
continue;
|
|
2072
2096
|
const value = ref[name];
|
|
2073
2097
|
if (value !== undefined && value !== null)
|
|
@@ -2077,7 +2101,7 @@
|
|
|
2077
2101
|
}
|
|
2078
2102
|
|
|
2079
2103
|
/**
|
|
2080
|
-
* Filter / unreliable /
|
|
2104
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
2081
2105
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
2082
2106
|
* the parent field's annotation + the parent tree's own state.
|
|
2083
2107
|
*/
|
|
@@ -2090,7 +2114,7 @@
|
|
|
2090
2114
|
checkInheritedFlags(tree, parent, parentIndex);
|
|
2091
2115
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
2092
2116
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
2093
|
-
if (tree.
|
|
2117
|
+
if (tree.isFullStateOnly)
|
|
2094
2118
|
return;
|
|
2095
2119
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
2096
2120
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -2116,7 +2140,7 @@
|
|
|
2116
2140
|
}
|
|
2117
2141
|
}
|
|
2118
2142
|
/**
|
|
2119
|
-
* Inherit filter / unreliable /
|
|
2143
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
2120
2144
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
2121
2145
|
* etc.) inherit these from the Schema field that holds them.
|
|
2122
2146
|
*
|
|
@@ -2155,14 +2179,14 @@
|
|
|
2155
2179
|
parentIndex = parentChangeTree.parentIndex;
|
|
2156
2180
|
}
|
|
2157
2181
|
const parentMetadata = parent?.constructor?.[Symbol.metadata];
|
|
2158
|
-
// Flag inheritance — pack the
|
|
2182
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
2159
2183
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
2160
2184
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
2161
2185
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
2162
2186
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
2163
2187
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
2164
|
-
const fieldBits = (parentMetadata?.[$
|
|
2165
|
-
| (parentMetadata?.[$
|
|
2188
|
+
const fieldBits = (parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
2189
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
2166
2190
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
2167
2191
|
const beforeFlags = tree.flags;
|
|
2168
2192
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -2172,7 +2196,7 @@
|
|
|
2172
2196
|
// `new Config().assign({...})` populates the recorder before the
|
|
2173
2197
|
// Config instance is attached). Static trees ship state via structural
|
|
2174
2198
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
2175
|
-
if (gainedBits &
|
|
2199
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
2176
2200
|
tree.reset();
|
|
2177
2201
|
tree.unreliableRecorder?.reset();
|
|
2178
2202
|
}
|
|
@@ -2477,7 +2501,7 @@
|
|
|
2477
2501
|
*
|
|
2478
2502
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
2479
2503
|
* - liveIteration.ts forEachLive
|
|
2480
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
2504
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
2481
2505
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
2482
2506
|
*
|
|
2483
2507
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -2497,12 +2521,12 @@
|
|
|
2497
2521
|
const _invokeNoCtx = (cb, index, op) => cb(index, op);
|
|
2498
2522
|
// Linked list helper functions
|
|
2499
2523
|
function createChangeTreeList() {
|
|
2500
|
-
return { next: undefined, tail: undefined };
|
|
2524
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
2501
2525
|
}
|
|
2502
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
2526
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
2503
2527
|
// field's annotation — inherited at setParent/setRoot time.
|
|
2504
2528
|
const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
2505
|
-
const IS_UNRELIABLE = 8,
|
|
2529
|
+
const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
2506
2530
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
2507
2531
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
2508
2532
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -2529,7 +2553,7 @@
|
|
|
2529
2553
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
2530
2554
|
* field mutations only) is designed later.
|
|
2531
2555
|
*/
|
|
2532
|
-
const INHERITABLE_FLAGS =
|
|
2556
|
+
const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
2533
2557
|
class ChangeTree {
|
|
2534
2558
|
ref;
|
|
2535
2559
|
/**
|
|
@@ -2548,8 +2572,8 @@
|
|
|
2548
2572
|
refTarget;
|
|
2549
2573
|
metadata;
|
|
2550
2574
|
/**
|
|
2551
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
2552
|
-
*
|
|
2575
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
2576
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
2553
2577
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
2554
2578
|
* EncodeDescriptor.ts.
|
|
2555
2579
|
*/
|
|
@@ -2620,10 +2644,10 @@
|
|
|
2620
2644
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2621
2645
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2622
2646
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2623
|
-
get
|
|
2624
|
-
set
|
|
2625
|
-
get
|
|
2626
|
-
set
|
|
2647
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
2648
|
+
set isPatchOnly(v) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
2649
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
2650
|
+
set isFullStateOnly(v) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
2627
2651
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2628
2652
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2629
2653
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2632,7 +2656,7 @@
|
|
|
2632
2656
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2633
2657
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2634
2658
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2635
|
-
// pattern as `
|
|
2659
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2636
2660
|
get hasFilteredFields() {
|
|
2637
2661
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2638
2662
|
}
|
|
@@ -2656,7 +2680,7 @@
|
|
|
2656
2680
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2657
2681
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2658
2682
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2659
|
-
// (
|
|
2683
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2660
2684
|
const desc = this.encDescriptor;
|
|
2661
2685
|
if (!desc.hasAnyUnreliable)
|
|
2662
2686
|
return false;
|
|
@@ -2666,15 +2690,15 @@
|
|
|
2666
2690
|
}
|
|
2667
2691
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2668
2692
|
// by the tracker (the value still lives on the instance).
|
|
2669
|
-
|
|
2670
|
-
if (this.
|
|
2693
|
+
isFieldFullStateOnly(index) {
|
|
2694
|
+
if (this.isFullStateOnly)
|
|
2671
2695
|
return true;
|
|
2672
2696
|
const desc = this.encDescriptor;
|
|
2673
|
-
if (!desc.
|
|
2697
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2674
2698
|
return false;
|
|
2675
2699
|
if (index < 32)
|
|
2676
|
-
return (desc.
|
|
2677
|
-
return Metadata.
|
|
2700
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2701
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2678
2702
|
}
|
|
2679
2703
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2680
2704
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2933,7 +2957,7 @@
|
|
|
2933
2957
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2934
2958
|
this.unreliableRecorder?.reset();
|
|
2935
2959
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2936
|
-
// (FILTERED/
|
|
2960
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2937
2961
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2938
2962
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2939
2963
|
this._fullSyncGen = 0;
|
|
@@ -2966,7 +2990,7 @@
|
|
|
2966
2990
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2967
2991
|
const src = this.collDirty;
|
|
2968
2992
|
const dst = new Map();
|
|
2969
|
-
const track = !this.paused && !this.
|
|
2993
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2970
2994
|
if (track) {
|
|
2971
2995
|
for (let i = 0; i < count; i++)
|
|
2972
2996
|
dst.set(i, exports.OPERATION.ADD);
|
|
@@ -2990,7 +3014,7 @@
|
|
|
2990
3014
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2991
3015
|
}
|
|
2992
3016
|
operation(op) {
|
|
2993
|
-
if (this.paused || this.
|
|
3017
|
+
if (this.paused || this.isFullStateOnly)
|
|
2994
3018
|
return;
|
|
2995
3019
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2996
3020
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3020,11 +3044,23 @@
|
|
|
3020
3044
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3021
3045
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3022
3046
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
3047
|
+
*
|
|
3048
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
3049
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
3050
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
3051
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
3052
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
3053
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
3054
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
3055
|
+
* which has always seeded these fields for late joiners.
|
|
3056
|
+
*
|
|
3057
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
3058
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
3023
3059
|
*/
|
|
3024
3060
|
_routeAndRecord(index, op, raw) {
|
|
3025
|
-
if (this.paused || this.
|
|
3061
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3026
3062
|
return;
|
|
3027
|
-
if (this.isFieldUnreliable(index)) {
|
|
3063
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3028
3064
|
const r = this.ensureUnreliableRecorder();
|
|
3029
3065
|
if (raw)
|
|
3030
3066
|
r.recordRaw(index, op);
|
|
@@ -3091,9 +3127,11 @@
|
|
|
3091
3127
|
}
|
|
3092
3128
|
return;
|
|
3093
3129
|
}
|
|
3094
|
-
if (this.paused || this.
|
|
3130
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3095
3131
|
return this.getValue(index);
|
|
3096
|
-
|
|
3132
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
3133
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
3134
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
3097
3135
|
if (unreliable)
|
|
3098
3136
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? exports.OPERATION.DELETE);
|
|
3099
3137
|
else
|
|
@@ -3152,6 +3190,8 @@
|
|
|
3152
3190
|
get parent() { return this.parentRef; }
|
|
3153
3191
|
get parentIndex() { return this._parentIndex; }
|
|
3154
3192
|
addParent(parent, index) { addParent(this, parent, index); }
|
|
3193
|
+
/** Re-point an existing parent's cached index after the parent reindexed. */
|
|
3194
|
+
setParentIndex(parent, index) { setParentIndex(this, parent, index); }
|
|
3155
3195
|
/** @returns true if parent was found and removed */
|
|
3156
3196
|
removeParent(parent = this.parent) { return removeParent(this, parent); }
|
|
3157
3197
|
findParent(predicate) {
|
|
@@ -3196,6 +3236,7 @@
|
|
|
3196
3236
|
operation() { }
|
|
3197
3237
|
setParent() { }
|
|
3198
3238
|
addParent() { }
|
|
3239
|
+
setParentIndex() { }
|
|
3199
3240
|
removeParent() { return false; }
|
|
3200
3241
|
getChange() { return 0; }
|
|
3201
3242
|
discard() { }
|
|
@@ -3299,7 +3340,9 @@
|
|
|
3299
3340
|
* @private
|
|
3300
3341
|
*/
|
|
3301
3342
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3302
|
-
// "compress" field index + operation
|
|
3343
|
+
// "compress" field index + operation. Can't collide with
|
|
3344
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
3345
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
3303
3346
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3304
3347
|
// Do not encode value for DELETE operations
|
|
3305
3348
|
if (operation === exports.OPERATION.DELETE) {
|
|
@@ -3495,8 +3538,8 @@
|
|
|
3495
3538
|
/**
|
|
3496
3539
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3497
3540
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3498
|
-
* of full-sync" (@
|
|
3499
|
-
* live data. Reflected clients have no @
|
|
3541
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
3542
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
3500
3543
|
* presence is the only reliable signal.
|
|
3501
3544
|
*/
|
|
3502
3545
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3510,7 +3553,7 @@
|
|
|
3510
3553
|
* entry the snapshot did not visit.
|
|
3511
3554
|
*
|
|
3512
3555
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3513
|
-
* `@
|
|
3556
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3514
3557
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3515
3558
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3516
3559
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3535,11 +3578,11 @@
|
|
|
3535
3578
|
if (refIndexes === undefined) {
|
|
3536
3579
|
return;
|
|
3537
3580
|
}
|
|
3538
|
-
const
|
|
3581
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3539
3582
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3540
3583
|
const fieldIndex = refIndexes[i];
|
|
3541
|
-
// @
|
|
3542
|
-
if (
|
|
3584
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3585
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3543
3586
|
continue;
|
|
3544
3587
|
}
|
|
3545
3588
|
const field = metadata[fieldIndex];
|
|
@@ -3564,7 +3607,7 @@
|
|
|
3564
3607
|
seen.add(refId);
|
|
3565
3608
|
// `undefined` = the collection never appeared in the payload at all
|
|
3566
3609
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3567
|
-
// (@
|
|
3610
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3568
3611
|
// means "present with zero entries" → prune everything.
|
|
3569
3612
|
const visited = decoder.resyncVisited.get(refId);
|
|
3570
3613
|
if (visited === undefined) {
|
|
@@ -4249,6 +4292,40 @@
|
|
|
4249
4292
|
// beyond the live range: appends land after the staged tmpItems tail
|
|
4250
4293
|
return tmpItems.length + (index - live);
|
|
4251
4294
|
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
4297
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
4298
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
4299
|
+
* at whichever element inherited the slot (issue #231).
|
|
4300
|
+
*
|
|
4301
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
4302
|
+
* the only reader and reaches the index only through a filtered array
|
|
4303
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
4304
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
4305
|
+
* its children every tick.
|
|
4306
|
+
*
|
|
4307
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
4308
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
4309
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
4310
|
+
*/
|
|
4311
|
+
$reindexChildren(from, staged) {
|
|
4312
|
+
if (!this[$changes].hasFilteredFields) {
|
|
4313
|
+
return;
|
|
4314
|
+
} // nothing will read the cache
|
|
4315
|
+
if (typeof this[$childType] === "string") {
|
|
4316
|
+
return;
|
|
4317
|
+
} // primitives have no child tree
|
|
4318
|
+
const tmpItems = this.tmpItems;
|
|
4319
|
+
const length = tmpItems.length;
|
|
4320
|
+
if (staged !== undefined) {
|
|
4321
|
+
while (from < length && tmpItems[from] === staged[from]) {
|
|
4322
|
+
from++;
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
for (let i = from; i < length; i++) {
|
|
4326
|
+
tmpItems[i]?.[$changes]?.setParentIndex(this, i);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4252
4329
|
// encoding only. Returns the wire index the change was recorded at
|
|
4253
4330
|
// (undefined when nothing was recorded).
|
|
4254
4331
|
$changeAt(index, value) {
|
|
@@ -4360,6 +4437,7 @@
|
|
|
4360
4437
|
self[$changes].operation(exports.OPERATION.REVERSE);
|
|
4361
4438
|
self.items.reverse();
|
|
4362
4439
|
self.tmpItems.reverse();
|
|
4440
|
+
self.$reindexChildren(0);
|
|
4363
4441
|
return this;
|
|
4364
4442
|
}
|
|
4365
4443
|
/**
|
|
@@ -4411,6 +4489,7 @@
|
|
|
4411
4489
|
// wouldn't OPERATION.MOVE make more sense here?
|
|
4412
4490
|
sortedItems.forEach((_, i) => changeTree.change(i, exports.OPERATION.REPLACE));
|
|
4413
4491
|
self.tmpItems.sort(compareFn);
|
|
4492
|
+
self.$reindexChildren(0);
|
|
4414
4493
|
self.isMovingItems = false;
|
|
4415
4494
|
return this;
|
|
4416
4495
|
}
|
|
@@ -4492,6 +4571,7 @@
|
|
|
4492
4571
|
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
4493
4572
|
}
|
|
4494
4573
|
self.tmpItems.unshift(...items);
|
|
4574
|
+
self.$reindexChildren(items.length); // survivors only — the loop above placed the new items
|
|
4495
4575
|
return self.items.unshift(...items);
|
|
4496
4576
|
}
|
|
4497
4577
|
/**
|
|
@@ -4760,8 +4840,14 @@
|
|
|
4760
4840
|
}
|
|
4761
4841
|
[$onEncodeEnd]() {
|
|
4762
4842
|
const self = this[$proxyTarget] ?? this;
|
|
4843
|
+
const staged = self.tmpItems;
|
|
4763
4844
|
self.tmpItems = self.items.slice();
|
|
4764
|
-
self.deletedIndexes.length
|
|
4845
|
+
if (self.deletedIndexes.length > 0) {
|
|
4846
|
+
// compaction just closed the staged holes — everything above the
|
|
4847
|
+
// lowest one slid down a slot
|
|
4848
|
+
self.$reindexChildren(0, staged);
|
|
4849
|
+
self.deletedIndexes.length = 0;
|
|
4850
|
+
}
|
|
4765
4851
|
}
|
|
4766
4852
|
[$onDecodeEnd]() {
|
|
4767
4853
|
const self = this[$proxyTarget] ?? this;
|
|
@@ -5883,7 +5969,7 @@
|
|
|
5883
5969
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5884
5970
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5885
5971
|
* propagate through the normal reliable channel without consuming the
|
|
5886
|
-
* per-tick budget. Chain `.
|
|
5972
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5887
5973
|
* post-add mutation tracking entirely.
|
|
5888
5974
|
*/
|
|
5889
5975
|
class StreamSchema {
|
|
@@ -6158,12 +6244,11 @@
|
|
|
6158
6244
|
_default = undefined;
|
|
6159
6245
|
_hasDefault = false;
|
|
6160
6246
|
_view = undefined;
|
|
6161
|
-
_owned = false;
|
|
6162
6247
|
_unreliable = false;
|
|
6163
|
-
|
|
6248
|
+
_patchOnly = false;
|
|
6164
6249
|
_deprecated = false;
|
|
6165
6250
|
_deprecatedThrows = true;
|
|
6166
|
-
|
|
6251
|
+
_fullStateOnly = false;
|
|
6167
6252
|
_stream = false;
|
|
6168
6253
|
_optional = false;
|
|
6169
6254
|
_noSync = false;
|
|
@@ -6198,38 +6283,43 @@
|
|
|
6198
6283
|
this._view = tag ?? -1;
|
|
6199
6284
|
return this;
|
|
6200
6285
|
}
|
|
6201
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6202
|
-
owned() {
|
|
6203
|
-
this._owned = true;
|
|
6204
|
-
return this;
|
|
6205
|
-
}
|
|
6206
6286
|
/**
|
|
6207
6287
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6208
6288
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6209
|
-
* tagged with `.
|
|
6289
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
6290
|
+
*
|
|
6291
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
6292
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
6293
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
6294
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
6295
|
+
* the field is never written again.
|
|
6210
6296
|
*/
|
|
6211
6297
|
unreliable() {
|
|
6212
6298
|
this._unreliable = true;
|
|
6213
6299
|
return this;
|
|
6214
6300
|
}
|
|
6215
6301
|
/**
|
|
6216
|
-
*
|
|
6217
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6218
|
-
* only after its next mutation is emitted on a
|
|
6219
|
-
* to `.unreliable()`.
|
|
6302
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
6303
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6304
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
6305
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
6220
6306
|
*/
|
|
6221
|
-
|
|
6222
|
-
this.
|
|
6307
|
+
patchOnly() {
|
|
6308
|
+
this._patchOnly = true;
|
|
6223
6309
|
return this;
|
|
6224
6310
|
}
|
|
6225
6311
|
/**
|
|
6226
|
-
*
|
|
6227
|
-
*
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
6312
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
6313
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
6314
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
6315
|
+
* The mirror of `.patchOnly()`.
|
|
6316
|
+
*
|
|
6317
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
6318
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
6319
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
6230
6320
|
*/
|
|
6231
|
-
|
|
6232
|
-
this.
|
|
6321
|
+
fullStateOnly() {
|
|
6322
|
+
this._fullStateOnly = true;
|
|
6233
6323
|
return this;
|
|
6234
6324
|
}
|
|
6235
6325
|
/**
|
|
@@ -6241,8 +6331,8 @@
|
|
|
6241
6331
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6242
6332
|
* want on the class for typing convenience without paying any sync cost.
|
|
6243
6333
|
*
|
|
6244
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6245
|
-
* `.unreliable()`, `.
|
|
6334
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6335
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6246
6336
|
* them throws at `schema()` time.
|
|
6247
6337
|
*
|
|
6248
6338
|
* ```ts
|
|
@@ -6284,9 +6374,12 @@
|
|
|
6284
6374
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6285
6375
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6286
6376
|
*
|
|
6377
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
6378
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
6379
|
+
*
|
|
6287
6380
|
* ```ts
|
|
6288
6381
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6289
|
-
* -
|
|
6382
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6290
6383
|
* )
|
|
6291
6384
|
* ```
|
|
6292
6385
|
*/
|
|
@@ -6321,12 +6414,11 @@
|
|
|
6321
6414
|
default: this._default,
|
|
6322
6415
|
hasDefault: this._hasDefault,
|
|
6323
6416
|
view: this._view,
|
|
6324
|
-
owned: this._owned,
|
|
6325
6417
|
unreliable: this._unreliable,
|
|
6326
|
-
|
|
6418
|
+
patchOnly: this._patchOnly,
|
|
6327
6419
|
deprecated: this._deprecated,
|
|
6328
6420
|
deprecatedThrows: this._deprecatedThrows,
|
|
6329
|
-
|
|
6421
|
+
fullStateOnly: this._fullStateOnly,
|
|
6330
6422
|
stream: this._stream,
|
|
6331
6423
|
optional: this._optional,
|
|
6332
6424
|
noSync: this._noSync,
|
|
@@ -6580,25 +6672,44 @@
|
|
|
6580
6672
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6581
6673
|
};
|
|
6582
6674
|
}
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6675
|
+
/**
|
|
6676
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
6677
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
6678
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
6679
|
+
*
|
|
6680
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
6681
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
6682
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
6683
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
6684
|
+
* written again.
|
|
6685
|
+
*/
|
|
6587
6686
|
function unreliable(target, field) {
|
|
6588
6687
|
const metadata = Metadata.initialize(target.constructor);
|
|
6589
6688
|
Metadata.setUnreliable(metadata, field);
|
|
6590
6689
|
}
|
|
6591
6690
|
/**
|
|
6592
|
-
* @
|
|
6593
|
-
* encodeAllView).
|
|
6691
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6692
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6594
6693
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6595
6694
|
* the next mutation.
|
|
6596
6695
|
*
|
|
6597
6696
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6598
6697
|
*/
|
|
6599
|
-
function
|
|
6698
|
+
function patchOnly(target, field) {
|
|
6699
|
+
const metadata = Metadata.initialize(target.constructor);
|
|
6700
|
+
Metadata.setPatchOnly(metadata, field);
|
|
6701
|
+
}
|
|
6702
|
+
/**
|
|
6703
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
6704
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
6705
|
+
* client has joined are not propagated to it — populate these fields
|
|
6706
|
+
* before clients connect (e.g. during onCreate).
|
|
6707
|
+
*
|
|
6708
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
6709
|
+
*/
|
|
6710
|
+
function fullStateOnly(target, field) {
|
|
6600
6711
|
const metadata = Metadata.initialize(target.constructor);
|
|
6601
|
-
Metadata.
|
|
6712
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6602
6713
|
}
|
|
6603
6714
|
function type(type, options) {
|
|
6604
6715
|
return function (target, field) {
|
|
@@ -6970,11 +7081,10 @@
|
|
|
6970
7081
|
}
|
|
6971
7082
|
};
|
|
6972
7083
|
const viewTagFields = {};
|
|
6973
|
-
const ownedFields = [];
|
|
6974
7084
|
const unreliableFields = [];
|
|
6975
|
-
const
|
|
7085
|
+
const patchOnlyFields = [];
|
|
6976
7086
|
const deprecatedFields = {};
|
|
6977
|
-
const
|
|
7087
|
+
const fullStateOnlyFields = [];
|
|
6978
7088
|
const streamFields = [];
|
|
6979
7089
|
const streamPriorityFields = {};
|
|
6980
7090
|
const optionalFields = [];
|
|
@@ -6986,15 +7096,22 @@
|
|
|
6986
7096
|
// Local-only field: skip metadata registration entirely so it is
|
|
6987
7097
|
// never encoded/decoded, but still seed its construction default
|
|
6988
7098
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6989
|
-
if (def.view !== undefined || def.
|
|
6990
|
-
def.
|
|
7099
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7100
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6991
7101
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6992
|
-
`together with a sync-only modifier (.view/.
|
|
7102
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6993
7103
|
`A local-only field cannot be synchronized.`);
|
|
6994
7104
|
}
|
|
6995
7105
|
seedDefault(fieldName, def);
|
|
6996
7106
|
continue;
|
|
6997
7107
|
}
|
|
7108
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
7109
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
7110
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
7111
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
7112
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
7113
|
+
`so the field would never reach a client — use .noSync() if that is intended.`);
|
|
7114
|
+
}
|
|
6998
7115
|
const normalizedType = getNormalizedType(def.type);
|
|
6999
7116
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
7000
7117
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -7005,20 +7122,17 @@
|
|
|
7005
7122
|
if (def.view !== undefined) {
|
|
7006
7123
|
viewTagFields[fieldName] = def.view;
|
|
7007
7124
|
}
|
|
7008
|
-
if (def.owned) {
|
|
7009
|
-
ownedFields.push(fieldName);
|
|
7010
|
-
}
|
|
7011
7125
|
if (def.unreliable) {
|
|
7012
7126
|
unreliableFields.push(fieldName);
|
|
7013
7127
|
}
|
|
7014
|
-
if (def.
|
|
7015
|
-
|
|
7128
|
+
if (def.patchOnly) {
|
|
7129
|
+
patchOnlyFields.push(fieldName);
|
|
7016
7130
|
}
|
|
7017
7131
|
if (def.deprecated) {
|
|
7018
7132
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7019
7133
|
}
|
|
7020
|
-
if (def.
|
|
7021
|
-
|
|
7134
|
+
if (def.fullStateOnly) {
|
|
7135
|
+
fullStateOnlyFields.push(fieldName);
|
|
7022
7136
|
}
|
|
7023
7137
|
if (def.stream) {
|
|
7024
7138
|
streamFields.push(fieldName);
|
|
@@ -7101,22 +7215,19 @@
|
|
|
7101
7215
|
for (const fieldName in viewTagFields) {
|
|
7102
7216
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7103
7217
|
}
|
|
7104
|
-
for (const fieldName of ownedFields) {
|
|
7105
|
-
owned(klass.prototype, fieldName);
|
|
7106
|
-
}
|
|
7107
7218
|
for (const fieldName of unreliableFields) {
|
|
7108
7219
|
unreliable(klass.prototype, fieldName);
|
|
7109
7220
|
}
|
|
7110
|
-
for (const fieldName of
|
|
7111
|
-
|
|
7221
|
+
for (const fieldName of patchOnlyFields) {
|
|
7222
|
+
patchOnly(klass.prototype, fieldName);
|
|
7112
7223
|
}
|
|
7113
7224
|
for (const fieldName in deprecatedFields) {
|
|
7114
7225
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7115
7226
|
}
|
|
7116
|
-
if (
|
|
7227
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7117
7228
|
const metadata = klass[Symbol.metadata];
|
|
7118
|
-
for (const fieldName of
|
|
7119
|
-
Metadata.
|
|
7229
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7230
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7120
7231
|
}
|
|
7121
7232
|
for (const fieldName of streamFields) {
|
|
7122
7233
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7765,7 +7876,7 @@
|
|
|
7765
7876
|
const previousRefCount = this.refCount[refId];
|
|
7766
7877
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7767
7878
|
//
|
|
7768
|
-
// Re-stage every currently-populated non-
|
|
7879
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7769
7880
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7770
7881
|
// re-emits it on the correct channel. Two triggers:
|
|
7771
7882
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7855,14 +7966,10 @@
|
|
|
7855
7966
|
const parentNode = parent[$changes][nodeField];
|
|
7856
7967
|
if (!parentNode || parentNode === node)
|
|
7857
7968
|
return;
|
|
7858
|
-
//
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
return; // already after parent
|
|
7863
|
-
cursor = cursor.next;
|
|
7864
|
-
}
|
|
7865
|
-
// If we reach here, node is before parent — need to move
|
|
7969
|
+
// Positions are strictly increasing along the list, so this is an
|
|
7970
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
7971
|
+
if (node.position > parentNode.position)
|
|
7972
|
+
return;
|
|
7866
7973
|
// Remove node from current position
|
|
7867
7974
|
if (node.prev) {
|
|
7868
7975
|
node.prev.next = node.next;
|
|
@@ -7876,16 +7983,18 @@
|
|
|
7876
7983
|
else {
|
|
7877
7984
|
changeSet.tail = node.prev;
|
|
7878
7985
|
}
|
|
7879
|
-
//
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7986
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
7987
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
7988
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
7989
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
7990
|
+
// a fresh max position, keeping the invariant append-only.
|
|
7991
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
7992
|
+
// subtree re-serializes parent-first behind it.)
|
|
7993
|
+
node.prev = changeSet.tail;
|
|
7994
|
+
node.next = undefined;
|
|
7995
|
+
changeSet.tail.next = node; // parentNode remains in the list — never empty here
|
|
7996
|
+
changeSet.tail = node;
|
|
7997
|
+
node.position = changeSet.nextPosition++;
|
|
7889
7998
|
}
|
|
7890
7999
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7891
8000
|
if (existingNode) {
|
|
@@ -7907,12 +8016,12 @@
|
|
|
7907
8016
|
node.changeTree = changeTree;
|
|
7908
8017
|
node.next = undefined;
|
|
7909
8018
|
node.prev = undefined;
|
|
7910
|
-
node.position = 0;
|
|
7911
8019
|
}
|
|
7912
8020
|
else {
|
|
7913
8021
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7914
8022
|
}
|
|
7915
8023
|
if (!list.next) {
|
|
8024
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7916
8025
|
list.next = node;
|
|
7917
8026
|
list.tail = node;
|
|
7918
8027
|
}
|
|
@@ -7921,6 +8030,7 @@
|
|
|
7921
8030
|
list.tail.next = node;
|
|
7922
8031
|
list.tail = node;
|
|
7923
8032
|
}
|
|
8033
|
+
node.position = list.nextPosition++;
|
|
7924
8034
|
return node;
|
|
7925
8035
|
}
|
|
7926
8036
|
/**
|
|
@@ -8019,6 +8129,7 @@
|
|
|
8019
8129
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8020
8130
|
ctx.isSchema = desc.isSchema;
|
|
8021
8131
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8132
|
+
ctx.tags = desc.tags;
|
|
8022
8133
|
ctx.structSwitchEmitted = false;
|
|
8023
8134
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8024
8135
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8058,10 +8169,13 @@
|
|
|
8058
8169
|
}
|
|
8059
8170
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8060
8171
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8061
|
-
// itself carries a @view tag.
|
|
8062
|
-
//
|
|
8172
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
8173
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
8174
|
+
// that arm needs a Schema with more than 32 fields.
|
|
8063
8175
|
const fieldFiltered = ctx.isSchema
|
|
8064
|
-
? (ctx.treeIsFiltered || (
|
|
8176
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8177
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8178
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8065
8179
|
: ctx.treeIsFiltered;
|
|
8066
8180
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8067
8181
|
return;
|
|
@@ -8116,7 +8230,7 @@
|
|
|
8116
8230
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8117
8231
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8118
8232
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8119
|
-
filterBitmask: 0,
|
|
8233
|
+
filterBitmask: 0, tags: undefined,
|
|
8120
8234
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8121
8235
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8122
8236
|
};
|
|
@@ -8173,6 +8287,7 @@
|
|
|
8173
8287
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8174
8288
|
ctx.isSchema = desc.isSchema;
|
|
8175
8289
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8290
|
+
ctx.tags = desc.tags;
|
|
8176
8291
|
ctx.structSwitchEmitted = false;
|
|
8177
8292
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8178
8293
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8424,7 +8539,7 @@
|
|
|
8424
8539
|
// Emit each element's full state — forEachLive walks populated
|
|
8425
8540
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8426
8541
|
// Covers both static elements (dirty state was reset by
|
|
8427
|
-
// inheritedFlags'
|
|
8542
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8428
8543
|
// has dirty state but the main loop skipped them because
|
|
8429
8544
|
// they're filtered).
|
|
8430
8545
|
for (const element of emittedElements) {
|
|
@@ -8503,20 +8618,81 @@
|
|
|
8503
8618
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8504
8619
|
// into `_stream.priority` when the stream was attached. Users
|
|
8505
8620
|
// can also override per-instance by assigning to the setter.
|
|
8621
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
8622
|
+
// wins over the declaration-scope one: it closes over the
|
|
8623
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
8624
|
+
const perView = st.priorityByView?.get(viewId);
|
|
8625
|
+
const usePerView = perView !== undefined;
|
|
8506
8626
|
const priority = st.priority;
|
|
8507
|
-
|
|
8508
|
-
//
|
|
8509
|
-
//
|
|
8627
|
+
const max = st.maxPerTick;
|
|
8628
|
+
// Select the `max` highest-priority candidates.
|
|
8629
|
+
//
|
|
8630
|
+
// A comparator-based sort invokes the callback twice per
|
|
8631
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
8632
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
8633
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
8634
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
8635
|
+
// the scratch by `max` rather than by the backlog.
|
|
8636
|
+
//
|
|
8637
|
+
// Ties keep the earlier position (both comparisons below are
|
|
8638
|
+
// strict), so equal-priority entries still drain in insertion
|
|
8639
|
+
// order.
|
|
8510
8640
|
const positions = [];
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8641
|
+
const stale = [];
|
|
8642
|
+
if (usePerView || priority !== undefined) {
|
|
8643
|
+
const bestPos = [];
|
|
8644
|
+
const bestScore = [];
|
|
8645
|
+
let filled = 0;
|
|
8646
|
+
for (const pos of pending) {
|
|
8647
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
8648
|
+
// through the same lookup regardless of $items layout.
|
|
8649
|
+
const element = s[$getByIndex](pos);
|
|
8650
|
+
if (element === undefined) {
|
|
8651
|
+
// Removed after being queued — drop it below without
|
|
8652
|
+
// spending budget on it.
|
|
8653
|
+
stale.push(pos);
|
|
8654
|
+
continue;
|
|
8655
|
+
}
|
|
8656
|
+
const score = usePerView
|
|
8657
|
+
? perView(element)
|
|
8658
|
+
: priority(view, element);
|
|
8659
|
+
// Window not yet full: always insert.
|
|
8660
|
+
if (filled < max) {
|
|
8661
|
+
let j = filled++;
|
|
8662
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8663
|
+
bestScore[j] = bestScore[j - 1];
|
|
8664
|
+
bestPos[j] = bestPos[j - 1];
|
|
8665
|
+
j--;
|
|
8666
|
+
}
|
|
8667
|
+
bestScore[j] = score;
|
|
8668
|
+
bestPos[j] = pos;
|
|
8669
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
8670
|
+
}
|
|
8671
|
+
else if (score > bestScore[max - 1]) {
|
|
8672
|
+
let j = max - 1;
|
|
8673
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8674
|
+
bestScore[j] = bestScore[j - 1];
|
|
8675
|
+
bestPos[j] = bestPos[j - 1];
|
|
8676
|
+
j--;
|
|
8677
|
+
}
|
|
8678
|
+
bestScore[j] = score;
|
|
8679
|
+
bestPos[j] = pos;
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
for (let i = 0; i < filled; i++)
|
|
8683
|
+
positions.push(bestPos[i]);
|
|
8517
8684
|
}
|
|
8518
|
-
|
|
8519
|
-
|
|
8685
|
+
else {
|
|
8686
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
8687
|
+
for (const pos of pending) {
|
|
8688
|
+
if (positions.length >= max)
|
|
8689
|
+
break;
|
|
8690
|
+
positions.push(pos);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
for (const pos of stale)
|
|
8694
|
+
pending.delete(pos);
|
|
8695
|
+
const count = positions.length;
|
|
8520
8696
|
let sent = st.sentByView.get(viewId);
|
|
8521
8697
|
if (sent === undefined) {
|
|
8522
8698
|
sent = new Set();
|
|
@@ -9871,6 +10047,31 @@
|
|
|
9871
10047
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9872
10048
|
root.releaseViewId(id);
|
|
9873
10049
|
});
|
|
10050
|
+
/**
|
|
10051
|
+
* Compact description of a rejected argument, for warning messages.
|
|
10052
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
10053
|
+
* populated collection inspects into dozens of lines of encoder
|
|
10054
|
+
* internals and buries the message that matters.
|
|
10055
|
+
*/
|
|
10056
|
+
function describeArg(value) {
|
|
10057
|
+
if (value === undefined) {
|
|
10058
|
+
return "undefined";
|
|
10059
|
+
}
|
|
10060
|
+
if (value === null) {
|
|
10061
|
+
return "null";
|
|
10062
|
+
}
|
|
10063
|
+
const type = typeof value;
|
|
10064
|
+
if (type === "string") {
|
|
10065
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
10066
|
+
}
|
|
10067
|
+
if (type !== "object" && type !== "function") {
|
|
10068
|
+
return `${type} ${String(value)}`;
|
|
10069
|
+
}
|
|
10070
|
+
if (Array.isArray(value)) {
|
|
10071
|
+
return `Array(${value.length})`;
|
|
10072
|
+
}
|
|
10073
|
+
return value.constructor?.name ?? "Object";
|
|
10074
|
+
}
|
|
9874
10075
|
class StateView {
|
|
9875
10076
|
iterable;
|
|
9876
10077
|
/**
|
|
@@ -10095,12 +10296,12 @@
|
|
|
10095
10296
|
}
|
|
10096
10297
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10097
10298
|
const changeTree = obj?.[$changes];
|
|
10098
|
-
const parentChangeTree = changeTree.parent;
|
|
10099
10299
|
if (!changeTree) {
|
|
10100
|
-
console.warn(
|
|
10300
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10101
10301
|
return false;
|
|
10102
10302
|
}
|
|
10103
|
-
|
|
10303
|
+
const parentChangeTree = changeTree.parent;
|
|
10304
|
+
if (!parentChangeTree &&
|
|
10104
10305
|
obj[$refId] !== 0 // allow root object
|
|
10105
10306
|
) {
|
|
10106
10307
|
/**
|
|
@@ -10381,9 +10582,9 @@
|
|
|
10381
10582
|
}
|
|
10382
10583
|
}
|
|
10383
10584
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10384
|
-
const changeTree = obj[$changes];
|
|
10585
|
+
const changeTree = obj?.[$changes];
|
|
10385
10586
|
if (!changeTree) {
|
|
10386
|
-
console.warn(
|
|
10587
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10387
10588
|
return this;
|
|
10388
10589
|
}
|
|
10389
10590
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10521,32 +10722,47 @@
|
|
|
10521
10722
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10522
10723
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10523
10724
|
}
|
|
10524
|
-
|
|
10525
|
-
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
10526
|
-
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
10527
|
-
* future content changes — every subsequent push / set / add to the
|
|
10528
|
-
* collection automatically flows to this view, and every removal
|
|
10529
|
-
* queues a DELETE op. Works on every collection type:
|
|
10530
|
-
*
|
|
10531
|
-
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
10532
|
-
* new children are force-shipped immediately (equivalent to
|
|
10533
|
-
* `view.add(child)` per item).
|
|
10534
|
-
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
10535
|
-
* enqueued into `_pendingByView` so the priority pass drains them
|
|
10536
|
-
* respecting `maxPerTick`.
|
|
10537
|
-
*
|
|
10538
|
-
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
10539
|
-
* collection is a no-op.
|
|
10540
|
-
*/
|
|
10541
|
-
subscribe(collection) {
|
|
10725
|
+
subscribe(collection, priority) {
|
|
10542
10726
|
const tree = collection?.[$changes];
|
|
10543
10727
|
if (!tree) {
|
|
10544
|
-
console.warn(
|
|
10728
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10545
10729
|
return this;
|
|
10546
10730
|
}
|
|
10547
10731
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10548
10732
|
this._bindRoot(tree.root);
|
|
10549
10733
|
}
|
|
10734
|
+
if (priority !== undefined) {
|
|
10735
|
+
if (!tree.isStreamCollection) {
|
|
10736
|
+
// Name the field rather than dumping the collection — a
|
|
10737
|
+
// populated MapSchema inspects into dozens of lines of
|
|
10738
|
+
// internals and buries the message.
|
|
10739
|
+
const kind = collection?.constructor?.name ?? "collection";
|
|
10740
|
+
const parent = tree.parent;
|
|
10741
|
+
if (parent === undefined) {
|
|
10742
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
10743
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
10744
|
+
`Subscribe after assigning it to the state.`);
|
|
10745
|
+
}
|
|
10746
|
+
else {
|
|
10747
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
10748
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
10749
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
10750
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
10751
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`);
|
|
10752
|
+
}
|
|
10753
|
+
}
|
|
10754
|
+
else {
|
|
10755
|
+
// Set before the idempotency return below, so re-subscribing
|
|
10756
|
+
// is the documented way to retarget this view's ordering.
|
|
10757
|
+
const st = ensureStreamState(collection);
|
|
10758
|
+
if (priority === null) {
|
|
10759
|
+
st.priorityByView?.delete(this.id);
|
|
10760
|
+
}
|
|
10761
|
+
else {
|
|
10762
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
10763
|
+
}
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10550
10766
|
if (this.isSubscribed(tree))
|
|
10551
10767
|
return this;
|
|
10552
10768
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10592,7 +10808,7 @@
|
|
|
10592
10808
|
unsubscribe(collection) {
|
|
10593
10809
|
const tree = collection?.[$changes];
|
|
10594
10810
|
if (!tree) {
|
|
10595
|
-
console.warn(
|
|
10811
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10596
10812
|
return this;
|
|
10597
10813
|
}
|
|
10598
10814
|
if (!this.isSubscribed(tree))
|
|
@@ -10721,15 +10937,15 @@
|
|
|
10721
10937
|
exports.encodeMapEntry = encodeMapEntry;
|
|
10722
10938
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
10723
10939
|
exports.entity = entity;
|
|
10940
|
+
exports.fullStateOnly = fullStateOnly;
|
|
10724
10941
|
exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
|
|
10725
10942
|
exports.getEncodeDescriptor = getEncodeDescriptor;
|
|
10726
10943
|
exports.getRawChangesCallback = getRawChangesCallback;
|
|
10727
10944
|
exports.isBuilder = isBuilder;
|
|
10728
|
-
exports.
|
|
10945
|
+
exports.patchOnly = patchOnly;
|
|
10729
10946
|
exports.registerType = registerType;
|
|
10730
10947
|
exports.schema = schema;
|
|
10731
10948
|
exports.t = t;
|
|
10732
|
-
exports.transient = transient;
|
|
10733
10949
|
exports.type = type;
|
|
10734
10950
|
exports.unreliable = unreliable;
|
|
10735
10951
|
exports.view = view;
|