@colyseus/schema 5.0.10 → 5.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Metadata.d.ts +20 -12
- package/build/annotations.d.ts +23 -10
- package/build/codegen/cli.cjs +615 -204
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/dart.d.ts +20 -0
- package/build/codegen/types.d.ts +20 -0
- package/build/decoder/Resync.d.ts +3 -3
- package/build/encoder/ChangeTree.d.ts +22 -10
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +393 -287
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +393 -287
- package/build/index.mjs +392 -286
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/StreamSchema.d.ts +1 -1
- package/build/types/symbols.d.ts +4 -10
- package/package.json +1 -1
- package/src/Metadata.ts +58 -31
- package/src/annotations.ts +56 -32
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/c.ts +21 -3
- package/src/codegen/languages/csharp.ts +7 -1
- package/src/codegen/languages/dart.ts +274 -0
- package/src/codegen/languages/haxe.ts +7 -1
- package/src/codegen/languages/lua.ts +16 -4
- package/src/codegen/languages/ts.ts +5 -0
- package/src/codegen/parser.ts +97 -3
- package/src/codegen/types.ts +24 -0
- package/src/decoder/Resync.ts +8 -8
- package/src/encoder/ChangeRecorder.ts +1 -1
- package/src/encoder/ChangeTree.ts +41 -28
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +100 -37
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +118 -47
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/streaming.ts +8 -0
- package/src/encoding/spec.ts +1 -1
- package/src/index.ts +2 -2
- package/src/types/builder.ts +35 -31
- package/src/types/custom/StreamSchema.ts +1 -1
- package/src/types/symbols.ts +4 -11
- package/src/bench_bloat.ts +0 -173
- package/src/bench_churn.ts +0 -121
- package/src/bench_decode.ts +0 -221
- package/src/bench_decode_mem.ts +0 -165
- package/src/bench_encode.ts +0 -108
- package/src/bench_init.ts +0 -150
- package/src/bench_static.ts +0 -109
- package/src/bench_stream.ts +0 -295
- package/src/bench_view_cmp.ts +0 -142
package/build/index.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,
|
|
@@ -1999,13 +1995,13 @@
|
|
|
1999
1995
|
}
|
|
2000
1996
|
|
|
2001
1997
|
/**
|
|
2002
|
-
* Walk all currently-populated non-
|
|
1998
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
2003
1999
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
2004
2000
|
* StateView.add to derive full-sync output from the live structure.
|
|
2005
2001
|
*
|
|
2006
|
-
*
|
|
2002
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
2007
2003
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
2008
|
-
* field is @
|
|
2004
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
2009
2005
|
*/
|
|
2010
2006
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
2011
2007
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
@@ -2019,10 +2015,10 @@
|
|
|
2019
2015
|
// types. See `ChangeTree.refTarget` doc.
|
|
2020
2016
|
const ref = tree.refTarget;
|
|
2021
2017
|
if (ref[$childType] !== undefined) {
|
|
2022
|
-
// Collection inheriting @
|
|
2018
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
2023
2019
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
2024
2020
|
// absent from full-sync output is never pruned client-side.
|
|
2025
|
-
if (tree.
|
|
2021
|
+
if (tree.isPatchOnly)
|
|
2026
2022
|
return;
|
|
2027
2023
|
// Collection types: dispatch by shape.
|
|
2028
2024
|
if (Array.isArray(ref.items)) {
|
|
@@ -2051,7 +2047,7 @@
|
|
|
2051
2047
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
2052
2048
|
// the setter records a DELETE when a field is set to null or
|
|
2053
2049
|
// undefined, so it should not appear in full-sync output.
|
|
2054
|
-
// (@
|
|
2050
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
2055
2051
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
2056
2052
|
//
|
|
2057
2053
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -2061,13 +2057,13 @@
|
|
|
2061
2057
|
if (!metadata)
|
|
2062
2058
|
return;
|
|
2063
2059
|
const numFields = (metadata[$numFields] ?? -1);
|
|
2064
|
-
const
|
|
2060
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
2065
2061
|
const names = tree.encDescriptor.names;
|
|
2066
2062
|
for (let i = 0; i <= numFields; i++) {
|
|
2067
2063
|
const name = names[i];
|
|
2068
2064
|
if (name === undefined)
|
|
2069
2065
|
continue;
|
|
2070
|
-
if (
|
|
2066
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i))
|
|
2071
2067
|
continue;
|
|
2072
2068
|
const value = ref[name];
|
|
2073
2069
|
if (value !== undefined && value !== null)
|
|
@@ -2077,7 +2073,7 @@
|
|
|
2077
2073
|
}
|
|
2078
2074
|
|
|
2079
2075
|
/**
|
|
2080
|
-
* Filter / unreliable /
|
|
2076
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
2081
2077
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
2082
2078
|
* the parent field's annotation + the parent tree's own state.
|
|
2083
2079
|
*/
|
|
@@ -2090,7 +2086,7 @@
|
|
|
2090
2086
|
checkInheritedFlags(tree, parent, parentIndex);
|
|
2091
2087
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
2092
2088
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
2093
|
-
if (tree.
|
|
2089
|
+
if (tree.isFullStateOnly)
|
|
2094
2090
|
return;
|
|
2095
2091
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
2096
2092
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -2116,7 +2112,7 @@
|
|
|
2116
2112
|
}
|
|
2117
2113
|
}
|
|
2118
2114
|
/**
|
|
2119
|
-
* Inherit filter / unreliable /
|
|
2115
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
2120
2116
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
2121
2117
|
* etc.) inherit these from the Schema field that holds them.
|
|
2122
2118
|
*
|
|
@@ -2155,14 +2151,14 @@
|
|
|
2155
2151
|
parentIndex = parentChangeTree.parentIndex;
|
|
2156
2152
|
}
|
|
2157
2153
|
const parentMetadata = parent?.constructor?.[Symbol.metadata];
|
|
2158
|
-
// Flag inheritance — pack the
|
|
2154
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
2159
2155
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
2160
2156
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
2161
2157
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
2162
2158
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
2163
2159
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
2164
|
-
const fieldBits = (parentMetadata?.[$
|
|
2165
|
-
| (parentMetadata?.[$
|
|
2160
|
+
const fieldBits = (parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
2161
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
2166
2162
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
2167
2163
|
const beforeFlags = tree.flags;
|
|
2168
2164
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -2172,7 +2168,7 @@
|
|
|
2172
2168
|
// `new Config().assign({...})` populates the recorder before the
|
|
2173
2169
|
// Config instance is attached). Static trees ship state via structural
|
|
2174
2170
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
2175
|
-
if (gainedBits &
|
|
2171
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
2176
2172
|
tree.reset();
|
|
2177
2173
|
tree.unreliableRecorder?.reset();
|
|
2178
2174
|
}
|
|
@@ -2477,7 +2473,7 @@
|
|
|
2477
2473
|
*
|
|
2478
2474
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
2479
2475
|
* - liveIteration.ts forEachLive
|
|
2480
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
2476
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
2481
2477
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
2482
2478
|
*
|
|
2483
2479
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -2497,12 +2493,12 @@
|
|
|
2497
2493
|
const _invokeNoCtx = (cb, index, op) => cb(index, op);
|
|
2498
2494
|
// Linked list helper functions
|
|
2499
2495
|
function createChangeTreeList() {
|
|
2500
|
-
return { next: undefined, tail: undefined };
|
|
2496
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
2501
2497
|
}
|
|
2502
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
2498
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
2503
2499
|
// field's annotation — inherited at setParent/setRoot time.
|
|
2504
2500
|
const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
2505
|
-
const IS_UNRELIABLE = 8,
|
|
2501
|
+
const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
2506
2502
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
2507
2503
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
2508
2504
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -2529,7 +2525,7 @@
|
|
|
2529
2525
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
2530
2526
|
* field mutations only) is designed later.
|
|
2531
2527
|
*/
|
|
2532
|
-
const INHERITABLE_FLAGS =
|
|
2528
|
+
const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
2533
2529
|
class ChangeTree {
|
|
2534
2530
|
ref;
|
|
2535
2531
|
/**
|
|
@@ -2548,8 +2544,8 @@
|
|
|
2548
2544
|
refTarget;
|
|
2549
2545
|
metadata;
|
|
2550
2546
|
/**
|
|
2551
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
2552
|
-
*
|
|
2547
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
2548
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
2553
2549
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
2554
2550
|
* EncodeDescriptor.ts.
|
|
2555
2551
|
*/
|
|
@@ -2598,7 +2594,6 @@
|
|
|
2598
2594
|
// per-view WeakSet lookups with direct bitwise ops.
|
|
2599
2595
|
// Lazy: undefined until the tree participates in any view.
|
|
2600
2596
|
visibleViews;
|
|
2601
|
-
invisibleViews;
|
|
2602
2597
|
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
2603
2598
|
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
2604
2599
|
tagViews;
|
|
@@ -2621,10 +2616,10 @@
|
|
|
2621
2616
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2622
2617
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2623
2618
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2624
|
-
get
|
|
2625
|
-
set
|
|
2626
|
-
get
|
|
2627
|
-
set
|
|
2619
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
2620
|
+
set isPatchOnly(v) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
2621
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
2622
|
+
set isFullStateOnly(v) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
2628
2623
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2629
2624
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2630
2625
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2633,7 +2628,7 @@
|
|
|
2633
2628
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2634
2629
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2635
2630
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2636
|
-
// pattern as `
|
|
2631
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2637
2632
|
get hasFilteredFields() {
|
|
2638
2633
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2639
2634
|
}
|
|
@@ -2657,7 +2652,7 @@
|
|
|
2657
2652
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2658
2653
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2659
2654
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2660
|
-
// (
|
|
2655
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2661
2656
|
const desc = this.encDescriptor;
|
|
2662
2657
|
if (!desc.hasAnyUnreliable)
|
|
2663
2658
|
return false;
|
|
@@ -2667,15 +2662,15 @@
|
|
|
2667
2662
|
}
|
|
2668
2663
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2669
2664
|
// by the tracker (the value still lives on the instance).
|
|
2670
|
-
|
|
2671
|
-
if (this.
|
|
2665
|
+
isFieldFullStateOnly(index) {
|
|
2666
|
+
if (this.isFullStateOnly)
|
|
2672
2667
|
return true;
|
|
2673
2668
|
const desc = this.encDescriptor;
|
|
2674
|
-
if (!desc.
|
|
2669
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2675
2670
|
return false;
|
|
2676
2671
|
if (index < 32)
|
|
2677
|
-
return (desc.
|
|
2678
|
-
return Metadata.
|
|
2672
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2673
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2679
2674
|
}
|
|
2680
2675
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2681
2676
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2934,7 +2929,7 @@
|
|
|
2934
2929
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2935
2930
|
this.unreliableRecorder?.reset();
|
|
2936
2931
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2937
|
-
// (FILTERED/
|
|
2932
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2938
2933
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2939
2934
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2940
2935
|
this._fullSyncGen = 0;
|
|
@@ -2950,7 +2945,6 @@
|
|
|
2950
2945
|
// per-view visibility lives on the tree (NOT keyed by refId), so a
|
|
2951
2946
|
// recycled tree must not inherit its previous life's view membership.
|
|
2952
2947
|
this.visibleViews = undefined;
|
|
2953
|
-
this.invisibleViews = undefined;
|
|
2954
2948
|
this.tagViews = undefined;
|
|
2955
2949
|
this.subscribedViews = undefined;
|
|
2956
2950
|
}
|
|
@@ -2968,7 +2962,7 @@
|
|
|
2968
2962
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2969
2963
|
const src = this.collDirty;
|
|
2970
2964
|
const dst = new Map();
|
|
2971
|
-
const track = !this.paused && !this.
|
|
2965
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2972
2966
|
if (track) {
|
|
2973
2967
|
for (let i = 0; i < count; i++)
|
|
2974
2968
|
dst.set(i, exports.OPERATION.ADD);
|
|
@@ -2992,7 +2986,7 @@
|
|
|
2992
2986
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2993
2987
|
}
|
|
2994
2988
|
operation(op) {
|
|
2995
|
-
if (this.paused || this.
|
|
2989
|
+
if (this.paused || this.isFullStateOnly)
|
|
2996
2990
|
return;
|
|
2997
2991
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2998
2992
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3022,11 +3016,23 @@
|
|
|
3022
3016
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3023
3017
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3024
3018
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
3019
|
+
*
|
|
3020
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
3021
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
3022
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
3023
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
3024
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
3025
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
3026
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
3027
|
+
* which has always seeded these fields for late joiners.
|
|
3028
|
+
*
|
|
3029
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
3030
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
3025
3031
|
*/
|
|
3026
3032
|
_routeAndRecord(index, op, raw) {
|
|
3027
|
-
if (this.paused || this.
|
|
3033
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3028
3034
|
return;
|
|
3029
|
-
if (this.isFieldUnreliable(index)) {
|
|
3035
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3030
3036
|
const r = this.ensureUnreliableRecorder();
|
|
3031
3037
|
if (raw)
|
|
3032
3038
|
r.recordRaw(index, op);
|
|
@@ -3093,9 +3099,11 @@
|
|
|
3093
3099
|
}
|
|
3094
3100
|
return;
|
|
3095
3101
|
}
|
|
3096
|
-
if (this.paused || this.
|
|
3102
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3097
3103
|
return this.getValue(index);
|
|
3098
|
-
|
|
3104
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
3105
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
3106
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
3099
3107
|
if (unreliable)
|
|
3100
3108
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? exports.OPERATION.DELETE);
|
|
3101
3109
|
else
|
|
@@ -3301,7 +3309,9 @@
|
|
|
3301
3309
|
* @private
|
|
3302
3310
|
*/
|
|
3303
3311
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3304
|
-
// "compress" field index + operation
|
|
3312
|
+
// "compress" field index + operation. Can't collide with
|
|
3313
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
3314
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
3305
3315
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3306
3316
|
// Do not encode value for DELETE operations
|
|
3307
3317
|
if (operation === exports.OPERATION.DELETE) {
|
|
@@ -3497,8 +3507,8 @@
|
|
|
3497
3507
|
/**
|
|
3498
3508
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3499
3509
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3500
|
-
* of full-sync" (@
|
|
3501
|
-
* live data. Reflected clients have no @
|
|
3510
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
3511
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
3502
3512
|
* presence is the only reliable signal.
|
|
3503
3513
|
*/
|
|
3504
3514
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3512,7 +3522,7 @@
|
|
|
3512
3522
|
* entry the snapshot did not visit.
|
|
3513
3523
|
*
|
|
3514
3524
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3515
|
-
* `@
|
|
3525
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3516
3526
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3517
3527
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3518
3528
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3537,11 +3547,11 @@
|
|
|
3537
3547
|
if (refIndexes === undefined) {
|
|
3538
3548
|
return;
|
|
3539
3549
|
}
|
|
3540
|
-
const
|
|
3550
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3541
3551
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3542
3552
|
const fieldIndex = refIndexes[i];
|
|
3543
|
-
// @
|
|
3544
|
-
if (
|
|
3553
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3554
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3545
3555
|
continue;
|
|
3546
3556
|
}
|
|
3547
3557
|
const field = metadata[fieldIndex];
|
|
@@ -3566,7 +3576,7 @@
|
|
|
3566
3576
|
seen.add(refId);
|
|
3567
3577
|
// `undefined` = the collection never appeared in the payload at all
|
|
3568
3578
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3569
|
-
// (@
|
|
3579
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3570
3580
|
// means "present with zero entries" → prune everything.
|
|
3571
3581
|
const visited = decoder.resyncVisited.get(refId);
|
|
3572
3582
|
if (visited === undefined) {
|
|
@@ -5885,7 +5895,7 @@
|
|
|
5885
5895
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5886
5896
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5887
5897
|
* propagate through the normal reliable channel without consuming the
|
|
5888
|
-
* per-tick budget. Chain `.
|
|
5898
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5889
5899
|
* post-add mutation tracking entirely.
|
|
5890
5900
|
*/
|
|
5891
5901
|
class StreamSchema {
|
|
@@ -6160,12 +6170,11 @@
|
|
|
6160
6170
|
_default = undefined;
|
|
6161
6171
|
_hasDefault = false;
|
|
6162
6172
|
_view = undefined;
|
|
6163
|
-
_owned = false;
|
|
6164
6173
|
_unreliable = false;
|
|
6165
|
-
|
|
6174
|
+
_patchOnly = false;
|
|
6166
6175
|
_deprecated = false;
|
|
6167
6176
|
_deprecatedThrows = true;
|
|
6168
|
-
|
|
6177
|
+
_fullStateOnly = false;
|
|
6169
6178
|
_stream = false;
|
|
6170
6179
|
_optional = false;
|
|
6171
6180
|
_noSync = false;
|
|
@@ -6200,38 +6209,43 @@
|
|
|
6200
6209
|
this._view = tag ?? -1;
|
|
6201
6210
|
return this;
|
|
6202
6211
|
}
|
|
6203
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6204
|
-
owned() {
|
|
6205
|
-
this._owned = true;
|
|
6206
|
-
return this;
|
|
6207
|
-
}
|
|
6208
6212
|
/**
|
|
6209
6213
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6210
6214
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6211
|
-
* tagged with `.
|
|
6215
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
6216
|
+
*
|
|
6217
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
6218
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
6219
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
6220
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
6221
|
+
* the field is never written again.
|
|
6212
6222
|
*/
|
|
6213
6223
|
unreliable() {
|
|
6214
6224
|
this._unreliable = true;
|
|
6215
6225
|
return this;
|
|
6216
6226
|
}
|
|
6217
6227
|
/**
|
|
6218
|
-
*
|
|
6219
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6220
|
-
* only after its next mutation is emitted on a
|
|
6221
|
-
* to `.unreliable()`.
|
|
6228
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
6229
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6230
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
6231
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
6222
6232
|
*/
|
|
6223
|
-
|
|
6224
|
-
this.
|
|
6233
|
+
patchOnly() {
|
|
6234
|
+
this._patchOnly = true;
|
|
6225
6235
|
return this;
|
|
6226
6236
|
}
|
|
6227
6237
|
/**
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
6230
|
-
*
|
|
6231
|
-
*
|
|
6238
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
6239
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
6240
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
6241
|
+
* The mirror of `.patchOnly()`.
|
|
6242
|
+
*
|
|
6243
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
6244
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
6245
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
6232
6246
|
*/
|
|
6233
|
-
|
|
6234
|
-
this.
|
|
6247
|
+
fullStateOnly() {
|
|
6248
|
+
this._fullStateOnly = true;
|
|
6235
6249
|
return this;
|
|
6236
6250
|
}
|
|
6237
6251
|
/**
|
|
@@ -6243,8 +6257,8 @@
|
|
|
6243
6257
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6244
6258
|
* want on the class for typing convenience without paying any sync cost.
|
|
6245
6259
|
*
|
|
6246
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6247
|
-
* `.unreliable()`, `.
|
|
6260
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6261
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6248
6262
|
* them throws at `schema()` time.
|
|
6249
6263
|
*
|
|
6250
6264
|
* ```ts
|
|
@@ -6286,9 +6300,12 @@
|
|
|
6286
6300
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6287
6301
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6288
6302
|
*
|
|
6303
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
6304
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
6305
|
+
*
|
|
6289
6306
|
* ```ts
|
|
6290
6307
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6291
|
-
* -
|
|
6308
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6292
6309
|
* )
|
|
6293
6310
|
* ```
|
|
6294
6311
|
*/
|
|
@@ -6323,12 +6340,11 @@
|
|
|
6323
6340
|
default: this._default,
|
|
6324
6341
|
hasDefault: this._hasDefault,
|
|
6325
6342
|
view: this._view,
|
|
6326
|
-
owned: this._owned,
|
|
6327
6343
|
unreliable: this._unreliable,
|
|
6328
|
-
|
|
6344
|
+
patchOnly: this._patchOnly,
|
|
6329
6345
|
deprecated: this._deprecated,
|
|
6330
6346
|
deprecatedThrows: this._deprecatedThrows,
|
|
6331
|
-
|
|
6347
|
+
fullStateOnly: this._fullStateOnly,
|
|
6332
6348
|
stream: this._stream,
|
|
6333
6349
|
optional: this._optional,
|
|
6334
6350
|
noSync: this._noSync,
|
|
@@ -6582,25 +6598,44 @@
|
|
|
6582
6598
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6583
6599
|
};
|
|
6584
6600
|
}
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6601
|
+
/**
|
|
6602
|
+
* `@unreliable` — route a field onto the unreliable transport channel, so a
|
|
6603
|
+
* dropped update costs one stale value instead of stalling the ordered stream
|
|
6604
|
+
* behind a retransmit. Primitive fields only (see `Metadata.setUnreliable`).
|
|
6605
|
+
*
|
|
6606
|
+
* The field's FIRST value still travels the reliable channel, as part of the
|
|
6607
|
+
* owning instance's ADD; only later mutations become unreliable. A decoder
|
|
6608
|
+
* cannot apply a write to a ref it has not been told about, so a value emitted
|
|
6609
|
+
* ahead of that ADD would be dropped — and lost for good if the field is never
|
|
6610
|
+
* written again.
|
|
6611
|
+
*/
|
|
6589
6612
|
function unreliable(target, field) {
|
|
6590
6613
|
const metadata = Metadata.initialize(target.constructor);
|
|
6591
6614
|
Metadata.setUnreliable(metadata, field);
|
|
6592
6615
|
}
|
|
6593
6616
|
/**
|
|
6594
|
-
* @
|
|
6595
|
-
* encodeAllView).
|
|
6617
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6618
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6596
6619
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6597
6620
|
* the next mutation.
|
|
6598
6621
|
*
|
|
6599
6622
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6600
6623
|
*/
|
|
6601
|
-
function
|
|
6624
|
+
function patchOnly(target, field) {
|
|
6625
|
+
const metadata = Metadata.initialize(target.constructor);
|
|
6626
|
+
Metadata.setPatchOnly(metadata, field);
|
|
6627
|
+
}
|
|
6628
|
+
/**
|
|
6629
|
+
* @fullStateOnly — mark a field as delivered in the full state sync only
|
|
6630
|
+
* (encodeAll / encodeAllView), never on per-tick patches. Writes after a
|
|
6631
|
+
* client has joined are not propagated to it — populate these fields
|
|
6632
|
+
* before clients connect (e.g. during onCreate).
|
|
6633
|
+
*
|
|
6634
|
+
* The exact mirror of @patchOnly — the two are mutually exclusive.
|
|
6635
|
+
*/
|
|
6636
|
+
function fullStateOnly(target, field) {
|
|
6602
6637
|
const metadata = Metadata.initialize(target.constructor);
|
|
6603
|
-
Metadata.
|
|
6638
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6604
6639
|
}
|
|
6605
6640
|
function type(type, options) {
|
|
6606
6641
|
return function (target, field) {
|
|
@@ -6972,11 +7007,10 @@
|
|
|
6972
7007
|
}
|
|
6973
7008
|
};
|
|
6974
7009
|
const viewTagFields = {};
|
|
6975
|
-
const ownedFields = [];
|
|
6976
7010
|
const unreliableFields = [];
|
|
6977
|
-
const
|
|
7011
|
+
const patchOnlyFields = [];
|
|
6978
7012
|
const deprecatedFields = {};
|
|
6979
|
-
const
|
|
7013
|
+
const fullStateOnlyFields = [];
|
|
6980
7014
|
const streamFields = [];
|
|
6981
7015
|
const streamPriorityFields = {};
|
|
6982
7016
|
const optionalFields = [];
|
|
@@ -6988,15 +7022,22 @@
|
|
|
6988
7022
|
// Local-only field: skip metadata registration entirely so it is
|
|
6989
7023
|
// never encoded/decoded, but still seed its construction default
|
|
6990
7024
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6991
|
-
if (def.view !== undefined || def.
|
|
6992
|
-
def.
|
|
7025
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7026
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6993
7027
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6994
|
-
`together with a sync-only modifier (.view/.
|
|
7028
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6995
7029
|
`A local-only field cannot be synchronized.`);
|
|
6996
7030
|
}
|
|
6997
7031
|
seedDefault(fieldName, def);
|
|
6998
7032
|
continue;
|
|
6999
7033
|
}
|
|
7034
|
+
// The two delivery channels are exhaustive: excluding a field from
|
|
7035
|
+
// both leaves it with nowhere to go — a silent .noSync().
|
|
7036
|
+
if (def.patchOnly && def.fullStateOnly) {
|
|
7037
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .patchOnly() ` +
|
|
7038
|
+
`together with .fullStateOnly(). Those are the only two delivery channels, ` +
|
|
7039
|
+
`so the field would never reach a client — use .noSync() if that is intended.`);
|
|
7040
|
+
}
|
|
7000
7041
|
const normalizedType = getNormalizedType(def.type);
|
|
7001
7042
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
7002
7043
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -7007,20 +7048,17 @@
|
|
|
7007
7048
|
if (def.view !== undefined) {
|
|
7008
7049
|
viewTagFields[fieldName] = def.view;
|
|
7009
7050
|
}
|
|
7010
|
-
if (def.owned) {
|
|
7011
|
-
ownedFields.push(fieldName);
|
|
7012
|
-
}
|
|
7013
7051
|
if (def.unreliable) {
|
|
7014
7052
|
unreliableFields.push(fieldName);
|
|
7015
7053
|
}
|
|
7016
|
-
if (def.
|
|
7017
|
-
|
|
7054
|
+
if (def.patchOnly) {
|
|
7055
|
+
patchOnlyFields.push(fieldName);
|
|
7018
7056
|
}
|
|
7019
7057
|
if (def.deprecated) {
|
|
7020
7058
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7021
7059
|
}
|
|
7022
|
-
if (def.
|
|
7023
|
-
|
|
7060
|
+
if (def.fullStateOnly) {
|
|
7061
|
+
fullStateOnlyFields.push(fieldName);
|
|
7024
7062
|
}
|
|
7025
7063
|
if (def.stream) {
|
|
7026
7064
|
streamFields.push(fieldName);
|
|
@@ -7103,22 +7141,19 @@
|
|
|
7103
7141
|
for (const fieldName in viewTagFields) {
|
|
7104
7142
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7105
7143
|
}
|
|
7106
|
-
for (const fieldName of ownedFields) {
|
|
7107
|
-
owned(klass.prototype, fieldName);
|
|
7108
|
-
}
|
|
7109
7144
|
for (const fieldName of unreliableFields) {
|
|
7110
7145
|
unreliable(klass.prototype, fieldName);
|
|
7111
7146
|
}
|
|
7112
|
-
for (const fieldName of
|
|
7113
|
-
|
|
7147
|
+
for (const fieldName of patchOnlyFields) {
|
|
7148
|
+
patchOnly(klass.prototype, fieldName);
|
|
7114
7149
|
}
|
|
7115
7150
|
for (const fieldName in deprecatedFields) {
|
|
7116
7151
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7117
7152
|
}
|
|
7118
|
-
if (
|
|
7153
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7119
7154
|
const metadata = klass[Symbol.metadata];
|
|
7120
|
-
for (const fieldName of
|
|
7121
|
-
Metadata.
|
|
7155
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7156
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7122
7157
|
}
|
|
7123
7158
|
for (const fieldName of streamFields) {
|
|
7124
7159
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7767,7 +7802,7 @@
|
|
|
7767
7802
|
const previousRefCount = this.refCount[refId];
|
|
7768
7803
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7769
7804
|
//
|
|
7770
|
-
// Re-stage every currently-populated non-
|
|
7805
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7771
7806
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7772
7807
|
// re-emits it on the correct channel. Two triggers:
|
|
7773
7808
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7857,14 +7892,10 @@
|
|
|
7857
7892
|
const parentNode = parent[$changes][nodeField];
|
|
7858
7893
|
if (!parentNode || parentNode === node)
|
|
7859
7894
|
return;
|
|
7860
|
-
//
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
return; // already after parent
|
|
7865
|
-
cursor = cursor.next;
|
|
7866
|
-
}
|
|
7867
|
-
// If we reach here, node is before parent — need to move
|
|
7895
|
+
// Positions are strictly increasing along the list, so this is an
|
|
7896
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
7897
|
+
if (node.position > parentNode.position)
|
|
7898
|
+
return;
|
|
7868
7899
|
// Remove node from current position
|
|
7869
7900
|
if (node.prev) {
|
|
7870
7901
|
node.prev.next = node.next;
|
|
@@ -7878,16 +7909,18 @@
|
|
|
7878
7909
|
else {
|
|
7879
7910
|
changeSet.tail = node.prev;
|
|
7880
7911
|
}
|
|
7881
|
-
//
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7912
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
7913
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
7914
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
7915
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
7916
|
+
// a fresh max position, keeping the invariant append-only.
|
|
7917
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
7918
|
+
// subtree re-serializes parent-first behind it.)
|
|
7919
|
+
node.prev = changeSet.tail;
|
|
7920
|
+
node.next = undefined;
|
|
7921
|
+
changeSet.tail.next = node; // parentNode remains in the list — never empty here
|
|
7922
|
+
changeSet.tail = node;
|
|
7923
|
+
node.position = changeSet.nextPosition++;
|
|
7891
7924
|
}
|
|
7892
7925
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7893
7926
|
if (existingNode) {
|
|
@@ -7909,12 +7942,12 @@
|
|
|
7909
7942
|
node.changeTree = changeTree;
|
|
7910
7943
|
node.next = undefined;
|
|
7911
7944
|
node.prev = undefined;
|
|
7912
|
-
node.position = 0;
|
|
7913
7945
|
}
|
|
7914
7946
|
else {
|
|
7915
7947
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7916
7948
|
}
|
|
7917
7949
|
if (!list.next) {
|
|
7950
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7918
7951
|
list.next = node;
|
|
7919
7952
|
list.tail = node;
|
|
7920
7953
|
}
|
|
@@ -7923,6 +7956,7 @@
|
|
|
7923
7956
|
list.tail.next = node;
|
|
7924
7957
|
list.tail = node;
|
|
7925
7958
|
}
|
|
7959
|
+
node.position = list.nextPosition++;
|
|
7926
7960
|
return node;
|
|
7927
7961
|
}
|
|
7928
7962
|
/**
|
|
@@ -8010,17 +8044,7 @@
|
|
|
8010
8044
|
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
8011
8045
|
// nothing itself but we still recurse so descendants (possibly added to
|
|
8012
8046
|
// the view explicitly) are reachable.
|
|
8013
|
-
|
|
8014
|
-
if (ctx.hasView) {
|
|
8015
|
-
const view = ctx.view;
|
|
8016
|
-
if (!view.isChangeTreeVisible(changeTree)) {
|
|
8017
|
-
view.markInvisible(changeTree);
|
|
8018
|
-
visibleHere = false;
|
|
8019
|
-
}
|
|
8020
|
-
else {
|
|
8021
|
-
view.unmarkInvisible(changeTree);
|
|
8022
|
-
}
|
|
8023
|
-
}
|
|
8047
|
+
const visibleHere = !ctx.hasView || ctx.view.isChangeTreeVisible(changeTree);
|
|
8024
8048
|
if (visibleHere) {
|
|
8025
8049
|
const desc = changeTree.encDescriptor;
|
|
8026
8050
|
ctx.changeTree = changeTree;
|
|
@@ -8031,6 +8055,7 @@
|
|
|
8031
8055
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8032
8056
|
ctx.isSchema = desc.isSchema;
|
|
8033
8057
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8058
|
+
ctx.tags = desc.tags;
|
|
8034
8059
|
ctx.structSwitchEmitted = false;
|
|
8035
8060
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8036
8061
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8070,10 +8095,13 @@
|
|
|
8070
8095
|
}
|
|
8071
8096
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8072
8097
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8073
|
-
// itself carries a @view tag.
|
|
8074
|
-
//
|
|
8098
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
8099
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
8100
|
+
// that arm needs a Schema with more than 32 fields.
|
|
8075
8101
|
const fieldFiltered = ctx.isSchema
|
|
8076
|
-
? (ctx.treeIsFiltered || (
|
|
8102
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8103
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8104
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8077
8105
|
: ctx.treeIsFiltered;
|
|
8078
8106
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8079
8107
|
return;
|
|
@@ -8128,7 +8156,7 @@
|
|
|
8128
8156
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8129
8157
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8130
8158
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8131
|
-
filterBitmask: 0,
|
|
8159
|
+
filterBitmask: 0, tags: undefined,
|
|
8132
8160
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8133
8161
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8134
8162
|
};
|
|
@@ -8169,12 +8197,8 @@
|
|
|
8169
8197
|
let current = queue;
|
|
8170
8198
|
while (current = current.next) {
|
|
8171
8199
|
const changeTree = current.changeTree;
|
|
8172
|
-
if (hasView) {
|
|
8173
|
-
|
|
8174
|
-
view.markInvisible(changeTree);
|
|
8175
|
-
continue;
|
|
8176
|
-
}
|
|
8177
|
-
view.unmarkInvisible(changeTree);
|
|
8200
|
+
if (hasView && !view.isChangeTreeVisible(changeTree)) {
|
|
8201
|
+
continue;
|
|
8178
8202
|
}
|
|
8179
8203
|
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
8180
8204
|
if (!recorder || !recorder.has()) {
|
|
@@ -8189,6 +8213,7 @@
|
|
|
8189
8213
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8190
8214
|
ctx.isSchema = desc.isSchema;
|
|
8191
8215
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8216
|
+
ctx.tags = desc.tags;
|
|
8192
8217
|
ctx.structSwitchEmitted = false;
|
|
8193
8218
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8194
8219
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8440,7 +8465,7 @@
|
|
|
8440
8465
|
// Emit each element's full state — forEachLive walks populated
|
|
8441
8466
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8442
8467
|
// Covers both static elements (dirty state was reset by
|
|
8443
|
-
// inheritedFlags'
|
|
8468
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8444
8469
|
// has dirty state but the main loop skipped them because
|
|
8445
8470
|
// they're filtered).
|
|
8446
8471
|
for (const element of emittedElements) {
|
|
@@ -8519,20 +8544,81 @@
|
|
|
8519
8544
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8520
8545
|
// into `_stream.priority` when the stream was attached. Users
|
|
8521
8546
|
// can also override per-instance by assigning to the setter.
|
|
8547
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
8548
|
+
// wins over the declaration-scope one: it closes over the
|
|
8549
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
8550
|
+
const perView = st.priorityByView?.get(viewId);
|
|
8551
|
+
const usePerView = perView !== undefined;
|
|
8522
8552
|
const priority = st.priority;
|
|
8523
|
-
|
|
8524
|
-
//
|
|
8525
|
-
//
|
|
8553
|
+
const max = st.maxPerTick;
|
|
8554
|
+
// Select the `max` highest-priority candidates.
|
|
8555
|
+
//
|
|
8556
|
+
// A comparator-based sort invokes the callback twice per
|
|
8557
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
8558
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
8559
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
8560
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
8561
|
+
// the scratch by `max` rather than by the backlog.
|
|
8562
|
+
//
|
|
8563
|
+
// Ties keep the earlier position (both comparisons below are
|
|
8564
|
+
// strict), so equal-priority entries still drain in insertion
|
|
8565
|
+
// order.
|
|
8526
8566
|
const positions = [];
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8567
|
+
const stale = [];
|
|
8568
|
+
if (usePerView || priority !== undefined) {
|
|
8569
|
+
const bestPos = [];
|
|
8570
|
+
const bestScore = [];
|
|
8571
|
+
let filled = 0;
|
|
8572
|
+
for (const pos of pending) {
|
|
8573
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
8574
|
+
// through the same lookup regardless of $items layout.
|
|
8575
|
+
const element = s[$getByIndex](pos);
|
|
8576
|
+
if (element === undefined) {
|
|
8577
|
+
// Removed after being queued — drop it below without
|
|
8578
|
+
// spending budget on it.
|
|
8579
|
+
stale.push(pos);
|
|
8580
|
+
continue;
|
|
8581
|
+
}
|
|
8582
|
+
const score = usePerView
|
|
8583
|
+
? perView(element)
|
|
8584
|
+
: priority(view, element);
|
|
8585
|
+
// Window not yet full: always insert.
|
|
8586
|
+
if (filled < max) {
|
|
8587
|
+
let j = filled++;
|
|
8588
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8589
|
+
bestScore[j] = bestScore[j - 1];
|
|
8590
|
+
bestPos[j] = bestPos[j - 1];
|
|
8591
|
+
j--;
|
|
8592
|
+
}
|
|
8593
|
+
bestScore[j] = score;
|
|
8594
|
+
bestPos[j] = pos;
|
|
8595
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
8596
|
+
}
|
|
8597
|
+
else if (score > bestScore[max - 1]) {
|
|
8598
|
+
let j = max - 1;
|
|
8599
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
8600
|
+
bestScore[j] = bestScore[j - 1];
|
|
8601
|
+
bestPos[j] = bestPos[j - 1];
|
|
8602
|
+
j--;
|
|
8603
|
+
}
|
|
8604
|
+
bestScore[j] = score;
|
|
8605
|
+
bestPos[j] = pos;
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8608
|
+
for (let i = 0; i < filled; i++)
|
|
8609
|
+
positions.push(bestPos[i]);
|
|
8533
8610
|
}
|
|
8534
|
-
|
|
8535
|
-
|
|
8611
|
+
else {
|
|
8612
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
8613
|
+
for (const pos of pending) {
|
|
8614
|
+
if (positions.length >= max)
|
|
8615
|
+
break;
|
|
8616
|
+
positions.push(pos);
|
|
8617
|
+
}
|
|
8618
|
+
}
|
|
8619
|
+
for (const pos of stale)
|
|
8620
|
+
pending.delete(pos);
|
|
8621
|
+
const count = positions.length;
|
|
8536
8622
|
let sent = st.sentByView.get(viewId);
|
|
8537
8623
|
if (sent === undefined) {
|
|
8538
8624
|
sent = new Set();
|
|
@@ -9866,9 +9952,6 @@
|
|
|
9866
9952
|
const v = tree.visibleViews;
|
|
9867
9953
|
if (v !== undefined && slot < v.length)
|
|
9868
9954
|
v[slot] &= clearMask;
|
|
9869
|
-
const i = tree.invisibleViews;
|
|
9870
|
-
if (i !== undefined && slot < i.length)
|
|
9871
|
-
i[slot] &= clearMask;
|
|
9872
9955
|
const s = tree.subscribedViews;
|
|
9873
9956
|
if (s !== undefined && slot < s.length)
|
|
9874
9957
|
s[slot] &= clearMask;
|
|
@@ -9890,6 +9973,31 @@
|
|
|
9890
9973
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9891
9974
|
root.releaseViewId(id);
|
|
9892
9975
|
});
|
|
9976
|
+
/**
|
|
9977
|
+
* Compact description of a rejected argument, for warning messages.
|
|
9978
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
9979
|
+
* populated collection inspects into dozens of lines of encoder
|
|
9980
|
+
* internals and buries the message that matters.
|
|
9981
|
+
*/
|
|
9982
|
+
function describeArg(value) {
|
|
9983
|
+
if (value === undefined) {
|
|
9984
|
+
return "undefined";
|
|
9985
|
+
}
|
|
9986
|
+
if (value === null) {
|
|
9987
|
+
return "null";
|
|
9988
|
+
}
|
|
9989
|
+
const type = typeof value;
|
|
9990
|
+
if (type === "string") {
|
|
9991
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
9992
|
+
}
|
|
9993
|
+
if (type !== "object" && type !== "function") {
|
|
9994
|
+
return `${type} ${String(value)}`;
|
|
9995
|
+
}
|
|
9996
|
+
if (Array.isArray(value)) {
|
|
9997
|
+
return `Array(${value.length})`;
|
|
9998
|
+
}
|
|
9999
|
+
return value.constructor?.name ?? "Object";
|
|
10000
|
+
}
|
|
9893
10001
|
class StateView {
|
|
9894
10002
|
iterable;
|
|
9895
10003
|
/**
|
|
@@ -10020,32 +10128,6 @@
|
|
|
10020
10128
|
if (slot < arr.length)
|
|
10021
10129
|
arr[slot] &= ~this._bit;
|
|
10022
10130
|
}
|
|
10023
|
-
/** True iff this view has previously marked `tree` as invisible. */
|
|
10024
|
-
isInvisible(tree) {
|
|
10025
|
-
const arr = tree.invisibleViews;
|
|
10026
|
-
const slot = this._slot;
|
|
10027
|
-
return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
|
|
10028
|
-
}
|
|
10029
|
-
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
10030
|
-
markInvisible(tree) {
|
|
10031
|
-
const slot = this._slot;
|
|
10032
|
-
let arr = tree.invisibleViews;
|
|
10033
|
-
if (arr === undefined) {
|
|
10034
|
-
arr = tree.invisibleViews = [];
|
|
10035
|
-
}
|
|
10036
|
-
while (arr.length <= slot)
|
|
10037
|
-
arr.push(0);
|
|
10038
|
-
arr[slot] |= this._bit;
|
|
10039
|
-
}
|
|
10040
|
-
/** Clear invisible bit. */
|
|
10041
|
-
unmarkInvisible(tree) {
|
|
10042
|
-
const arr = tree.invisibleViews;
|
|
10043
|
-
if (arr === undefined)
|
|
10044
|
-
return;
|
|
10045
|
-
const slot = this._slot;
|
|
10046
|
-
if (slot < arr.length)
|
|
10047
|
-
arr[slot] &= ~this._bit;
|
|
10048
|
-
}
|
|
10049
10131
|
// ──────────────────────────────────────────────────────────────────
|
|
10050
10132
|
// Per-tag, per-view bitmap. Replaces the legacy
|
|
10051
10133
|
// `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
|
|
@@ -10140,12 +10222,12 @@
|
|
|
10140
10222
|
}
|
|
10141
10223
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10142
10224
|
const changeTree = obj?.[$changes];
|
|
10143
|
-
const parentChangeTree = changeTree.parent;
|
|
10144
10225
|
if (!changeTree) {
|
|
10145
|
-
console.warn(
|
|
10226
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10146
10227
|
return false;
|
|
10147
10228
|
}
|
|
10148
|
-
|
|
10229
|
+
const parentChangeTree = changeTree.parent;
|
|
10230
|
+
if (!parentChangeTree &&
|
|
10149
10231
|
obj[$refId] !== 0 // allow root object
|
|
10150
10232
|
) {
|
|
10151
10233
|
/**
|
|
@@ -10189,11 +10271,18 @@
|
|
|
10189
10271
|
// below use `metadata?.[...]` null-safe access. Only Schema
|
|
10190
10272
|
// subclasses yield a real Metadata object.
|
|
10191
10273
|
const metadata = obj.constructor[Symbol.metadata];
|
|
10192
|
-
|
|
10193
|
-
//
|
|
10194
|
-
|
|
10274
|
+
// Add to iterable list (only the explicitly added items), deduping
|
|
10275
|
+
// re-adds of an already-visible instance. isVisible must be read
|
|
10276
|
+
// BEFORE markVisible; indexOf runs only on the re-add path.
|
|
10277
|
+
// NOTE: dedup applies to `items` only — a re-add still re-queues the
|
|
10278
|
+
// full snapshot on purpose (shared-view bootstrap re-add: a
|
|
10279
|
+
// late-attached client may not have consumed earlier drains).
|
|
10280
|
+
// Callers wanting cheap idempotence can guard with `view.has(obj)`.
|
|
10281
|
+
if (this.iterable && checkIncludeParent
|
|
10282
|
+
&& (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
|
|
10195
10283
|
this.items.push(obj);
|
|
10196
10284
|
}
|
|
10285
|
+
this.markVisible(changeTree);
|
|
10197
10286
|
// add parent ChangeTree's
|
|
10198
10287
|
// - if it was invisible to this view
|
|
10199
10288
|
// - if it were previously filtered out
|
|
@@ -10276,18 +10365,20 @@
|
|
|
10276
10365
|
}
|
|
10277
10366
|
else if (!changeTree.isNew || isChildAdded) {
|
|
10278
10367
|
// new structures will be added as part of .encode() call, no need to force it to .encodeView()
|
|
10279
|
-
const isInvisible = this.isInvisible(changeTree);
|
|
10280
10368
|
// Full-sync snapshot: walk the live ref structurally instead of
|
|
10281
10369
|
// iterating a cumulative recorder bucket. Every populated index
|
|
10282
10370
|
// is emitted as ADD (matching the op-coercion previously done
|
|
10283
10371
|
// at encode time). Per-field tags come from the descriptor's
|
|
10284
10372
|
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
10285
10373
|
// object hop.
|
|
10374
|
+
//
|
|
10375
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
10376
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
10377
|
+
// so anything added leaks straight to the wire.
|
|
10286
10378
|
const tags = changeTree.encDescriptor.tags;
|
|
10287
10379
|
changeTree.forEachLive((index) => {
|
|
10288
10380
|
const tagAtIndex = tags[index];
|
|
10289
|
-
if (
|
|
10290
|
-
tagAtIndex === undefined || // "all change" with no tag
|
|
10381
|
+
if (tagAtIndex === undefined || // "all change" with no tag
|
|
10291
10382
|
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
10292
10383
|
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
10293
10384
|
) {
|
|
@@ -10417,9 +10508,9 @@
|
|
|
10417
10508
|
}
|
|
10418
10509
|
}
|
|
10419
10510
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10420
|
-
const changeTree = obj[$changes];
|
|
10511
|
+
const changeTree = obj?.[$changes];
|
|
10421
10512
|
if (!changeTree) {
|
|
10422
|
-
console.warn(
|
|
10513
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10423
10514
|
return this;
|
|
10424
10515
|
}
|
|
10425
10516
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10439,7 +10530,7 @@
|
|
|
10439
10530
|
}
|
|
10440
10531
|
// ── Streamable-collection unsubscribe (the stream itself) ─────
|
|
10441
10532
|
// Flush DELETE for every sent position and drop pending. After
|
|
10442
|
-
// this, the stream is
|
|
10533
|
+
// this, the stream is no longer visible to this view — any future
|
|
10443
10534
|
// `stream.add()` would still seed broadcast pending (if no views)
|
|
10444
10535
|
// but would NOT re-seed per-view pending (user must re-subscribe).
|
|
10445
10536
|
if (changeTree.isStreamCollection) {
|
|
@@ -10557,32 +10648,47 @@
|
|
|
10557
10648
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10558
10649
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10559
10650
|
}
|
|
10560
|
-
|
|
10561
|
-
* Persistent subscription to a collection's contents. Unlike `add()`,
|
|
10562
|
-
* which is a one-shot bootstrap, `subscribe()` enrolls this view in
|
|
10563
|
-
* future content changes — every subsequent push / set / add to the
|
|
10564
|
-
* collection automatically flows to this view, and every removal
|
|
10565
|
-
* queues a DELETE op. Works on every collection type:
|
|
10566
|
-
*
|
|
10567
|
-
* - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
|
|
10568
|
-
* new children are force-shipped immediately (equivalent to
|
|
10569
|
-
* `view.add(child)` per item).
|
|
10570
|
-
* - `StreamSchema` (or `.stream()` maps/sets): new positions are
|
|
10571
|
-
* enqueued into `_pendingByView` so the priority pass drains them
|
|
10572
|
-
* respecting `maxPerTick`.
|
|
10573
|
-
*
|
|
10574
|
-
* Idempotent on re-subscribe. Subscribing to an already-subscribed
|
|
10575
|
-
* collection is a no-op.
|
|
10576
|
-
*/
|
|
10577
|
-
subscribe(collection) {
|
|
10651
|
+
subscribe(collection, priority) {
|
|
10578
10652
|
const tree = collection?.[$changes];
|
|
10579
10653
|
if (!tree) {
|
|
10580
|
-
console.warn(
|
|
10654
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10581
10655
|
return this;
|
|
10582
10656
|
}
|
|
10583
10657
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10584
10658
|
this._bindRoot(tree.root);
|
|
10585
10659
|
}
|
|
10660
|
+
if (priority !== undefined) {
|
|
10661
|
+
if (!tree.isStreamCollection) {
|
|
10662
|
+
// Name the field rather than dumping the collection — a
|
|
10663
|
+
// populated MapSchema inspects into dozens of lines of
|
|
10664
|
+
// internals and buries the message.
|
|
10665
|
+
const kind = collection?.constructor?.name ?? "collection";
|
|
10666
|
+
const parent = tree.parent;
|
|
10667
|
+
if (parent === undefined) {
|
|
10668
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
10669
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
10670
|
+
`Subscribe after assigning it to the state.`);
|
|
10671
|
+
}
|
|
10672
|
+
else {
|
|
10673
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
10674
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
10675
|
+
console.warn(`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
10676
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
10677
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`);
|
|
10678
|
+
}
|
|
10679
|
+
}
|
|
10680
|
+
else {
|
|
10681
|
+
// Set before the idempotency return below, so re-subscribing
|
|
10682
|
+
// is the documented way to retarget this view's ordering.
|
|
10683
|
+
const st = ensureStreamState(collection);
|
|
10684
|
+
if (priority === null) {
|
|
10685
|
+
st.priorityByView?.delete(this.id);
|
|
10686
|
+
}
|
|
10687
|
+
else {
|
|
10688
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
10689
|
+
}
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10586
10692
|
if (this.isSubscribed(tree))
|
|
10587
10693
|
return this;
|
|
10588
10694
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10628,7 +10734,7 @@
|
|
|
10628
10734
|
unsubscribe(collection) {
|
|
10629
10735
|
const tree = collection?.[$changes];
|
|
10630
10736
|
if (!tree) {
|
|
10631
|
-
console.warn(
|
|
10737
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10632
10738
|
return this;
|
|
10633
10739
|
}
|
|
10634
10740
|
if (!this.isSubscribed(tree))
|
|
@@ -10757,15 +10863,15 @@
|
|
|
10757
10863
|
exports.encodeMapEntry = encodeMapEntry;
|
|
10758
10864
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
10759
10865
|
exports.entity = entity;
|
|
10866
|
+
exports.fullStateOnly = fullStateOnly;
|
|
10760
10867
|
exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
|
|
10761
10868
|
exports.getEncodeDescriptor = getEncodeDescriptor;
|
|
10762
10869
|
exports.getRawChangesCallback = getRawChangesCallback;
|
|
10763
10870
|
exports.isBuilder = isBuilder;
|
|
10764
|
-
exports.
|
|
10871
|
+
exports.patchOnly = patchOnly;
|
|
10765
10872
|
exports.registerType = registerType;
|
|
10766
10873
|
exports.schema = schema;
|
|
10767
10874
|
exports.t = t;
|
|
10768
|
-
exports.transient = transient;
|
|
10769
10875
|
exports.type = type;
|
|
10770
10876
|
exports.unreliable = unreliable;
|
|
10771
10877
|
exports.view = view;
|