@colyseus/schema 5.0.11 → 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 -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/streaming.d.ts +7 -0
- package/build/index.cjs +374 -232
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +374 -232
- package/build/index.mjs +373 -231
- 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 -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/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
|
*/
|
|
@@ -2620,10 +2616,10 @@
|
|
|
2620
2616
|
set isNew(v) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
2621
2617
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
2622
2618
|
set isUnreliable(v) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
2623
|
-
get
|
|
2624
|
-
set
|
|
2625
|
-
get
|
|
2626
|
-
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); }
|
|
2627
2623
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
2628
2624
|
set isStreamCollection(v) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
2629
2625
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -2632,7 +2628,7 @@
|
|
|
2632
2628
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
2633
2629
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
2634
2630
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
2635
|
-
// pattern as `
|
|
2631
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
2636
2632
|
get hasFilteredFields() {
|
|
2637
2633
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
2638
2634
|
}
|
|
@@ -2656,7 +2652,7 @@
|
|
|
2656
2652
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
2657
2653
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
2658
2654
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
2659
|
-
// (
|
|
2655
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
2660
2656
|
const desc = this.encDescriptor;
|
|
2661
2657
|
if (!desc.hasAnyUnreliable)
|
|
2662
2658
|
return false;
|
|
@@ -2666,15 +2662,15 @@
|
|
|
2666
2662
|
}
|
|
2667
2663
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
2668
2664
|
// by the tracker (the value still lives on the instance).
|
|
2669
|
-
|
|
2670
|
-
if (this.
|
|
2665
|
+
isFieldFullStateOnly(index) {
|
|
2666
|
+
if (this.isFullStateOnly)
|
|
2671
2667
|
return true;
|
|
2672
2668
|
const desc = this.encDescriptor;
|
|
2673
|
-
if (!desc.
|
|
2669
|
+
if (!desc.hasAnyFullStateOnly)
|
|
2674
2670
|
return false;
|
|
2675
2671
|
if (index < 32)
|
|
2676
|
-
return (desc.
|
|
2677
|
-
return Metadata.
|
|
2672
|
+
return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
2673
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
2678
2674
|
}
|
|
2679
2675
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
2680
2676
|
// gate instead of emitting all dirty ADDs in one tick. Class-level short
|
|
@@ -2933,7 +2929,7 @@
|
|
|
2933
2929
|
// keep the recorder object allocated (re-alloc is the cost we avoid), clear contents
|
|
2934
2930
|
this.unreliableRecorder?.reset();
|
|
2935
2931
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
2936
|
-
// (FILTERED/
|
|
2932
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
2937
2933
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
2938
2934
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
2939
2935
|
this._fullSyncGen = 0;
|
|
@@ -2966,7 +2962,7 @@
|
|
|
2966
2962
|
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2967
2963
|
const src = this.collDirty;
|
|
2968
2964
|
const dst = new Map();
|
|
2969
|
-
const track = !this.paused && !this.
|
|
2965
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
2970
2966
|
if (track) {
|
|
2971
2967
|
for (let i = 0; i < count; i++)
|
|
2972
2968
|
dst.set(i, exports.OPERATION.ADD);
|
|
@@ -2990,7 +2986,7 @@
|
|
|
2990
2986
|
forEachLiveWithCtx(this, ctx, cb);
|
|
2991
2987
|
}
|
|
2992
2988
|
operation(op) {
|
|
2993
|
-
if (this.paused || this.
|
|
2989
|
+
if (this.paused || this.isFullStateOnly)
|
|
2994
2990
|
return;
|
|
2995
2991
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
2996
2992
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
@@ -3020,11 +3016,23 @@
|
|
|
3020
3016
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
3021
3017
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
3022
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`.
|
|
3023
3031
|
*/
|
|
3024
3032
|
_routeAndRecord(index, op, raw) {
|
|
3025
|
-
if (this.paused || this.
|
|
3033
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3026
3034
|
return;
|
|
3027
|
-
if (this.isFieldUnreliable(index)) {
|
|
3035
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
3028
3036
|
const r = this.ensureUnreliableRecorder();
|
|
3029
3037
|
if (raw)
|
|
3030
3038
|
r.recordRaw(index, op);
|
|
@@ -3091,9 +3099,11 @@
|
|
|
3091
3099
|
}
|
|
3092
3100
|
return;
|
|
3093
3101
|
}
|
|
3094
|
-
if (this.paused || this.
|
|
3102
|
+
if (this.paused || this.isFieldFullStateOnly(index))
|
|
3095
3103
|
return this.getValue(index);
|
|
3096
|
-
|
|
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;
|
|
3097
3107
|
if (unreliable)
|
|
3098
3108
|
this.ensureUnreliableRecorder().recordDelete(index, operation ?? exports.OPERATION.DELETE);
|
|
3099
3109
|
else
|
|
@@ -3299,7 +3309,9 @@
|
|
|
3299
3309
|
* @private
|
|
3300
3310
|
*/
|
|
3301
3311
|
const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __) {
|
|
3302
|
-
// "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.
|
|
3303
3315
|
bytes[it.offset++] = (index | operation) & 255;
|
|
3304
3316
|
// Do not encode value for DELETE operations
|
|
3305
3317
|
if (operation === exports.OPERATION.DELETE) {
|
|
@@ -3495,8 +3507,8 @@
|
|
|
3495
3507
|
/**
|
|
3496
3508
|
* Mark a collection as present in the payload — even with zero entries.
|
|
3497
3509
|
* The sweep only prunes collections reported here: absence means "not part
|
|
3498
|
-
* of full-sync" (@
|
|
3499
|
-
* 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
|
|
3500
3512
|
* presence is the only reliable signal.
|
|
3501
3513
|
*/
|
|
3502
3514
|
function resyncMarkPresent(decoder, refId) {
|
|
@@ -3510,7 +3522,7 @@
|
|
|
3510
3522
|
* entry the snapshot did not visit.
|
|
3511
3523
|
*
|
|
3512
3524
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
3513
|
-
* `@
|
|
3525
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
3514
3526
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
3515
3527
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
3516
3528
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -3535,11 +3547,11 @@
|
|
|
3535
3547
|
if (refIndexes === undefined) {
|
|
3536
3548
|
return;
|
|
3537
3549
|
}
|
|
3538
|
-
const
|
|
3550
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes];
|
|
3539
3551
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
3540
3552
|
const fieldIndex = refIndexes[i];
|
|
3541
|
-
// @
|
|
3542
|
-
if (
|
|
3553
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
3554
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) {
|
|
3543
3555
|
continue;
|
|
3544
3556
|
}
|
|
3545
3557
|
const field = metadata[fieldIndex];
|
|
@@ -3564,7 +3576,7 @@
|
|
|
3564
3576
|
seen.add(refId);
|
|
3565
3577
|
// `undefined` = the collection never appeared in the payload at all
|
|
3566
3578
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
3567
|
-
// (@
|
|
3579
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
3568
3580
|
// means "present with zero entries" → prune everything.
|
|
3569
3581
|
const visited = decoder.resyncVisited.get(refId);
|
|
3570
3582
|
if (visited === undefined) {
|
|
@@ -5883,7 +5895,7 @@
|
|
|
5883
5895
|
* per-client and drained in priority order (callback on StateView) up to
|
|
5884
5896
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
5885
5897
|
* propagate through the normal reliable channel without consuming the
|
|
5886
|
-
* per-tick budget. Chain `.
|
|
5898
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
5887
5899
|
* post-add mutation tracking entirely.
|
|
5888
5900
|
*/
|
|
5889
5901
|
class StreamSchema {
|
|
@@ -6158,12 +6170,11 @@
|
|
|
6158
6170
|
_default = undefined;
|
|
6159
6171
|
_hasDefault = false;
|
|
6160
6172
|
_view = undefined;
|
|
6161
|
-
_owned = false;
|
|
6162
6173
|
_unreliable = false;
|
|
6163
|
-
|
|
6174
|
+
_patchOnly = false;
|
|
6164
6175
|
_deprecated = false;
|
|
6165
6176
|
_deprecatedThrows = true;
|
|
6166
|
-
|
|
6177
|
+
_fullStateOnly = false;
|
|
6167
6178
|
_stream = false;
|
|
6168
6179
|
_optional = false;
|
|
6169
6180
|
_noSync = false;
|
|
@@ -6198,38 +6209,43 @@
|
|
|
6198
6209
|
this._view = tag ?? -1;
|
|
6199
6210
|
return this;
|
|
6200
6211
|
}
|
|
6201
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
6202
|
-
owned() {
|
|
6203
|
-
this._owned = true;
|
|
6204
|
-
return this;
|
|
6205
|
-
}
|
|
6206
6212
|
/**
|
|
6207
6213
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
6208
6214
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
6209
|
-
* 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.
|
|
6210
6222
|
*/
|
|
6211
6223
|
unreliable() {
|
|
6212
6224
|
this._unreliable = true;
|
|
6213
6225
|
return this;
|
|
6214
6226
|
}
|
|
6215
6227
|
/**
|
|
6216
|
-
*
|
|
6217
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
6218
|
-
* only after its next mutation is emitted on a
|
|
6219
|
-
* 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()`.
|
|
6220
6232
|
*/
|
|
6221
|
-
|
|
6222
|
-
this.
|
|
6233
|
+
patchOnly() {
|
|
6234
|
+
this._patchOnly = true;
|
|
6223
6235
|
return this;
|
|
6224
6236
|
}
|
|
6225
6237
|
/**
|
|
6226
|
-
*
|
|
6227
|
-
*
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
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.
|
|
6230
6246
|
*/
|
|
6231
|
-
|
|
6232
|
-
this.
|
|
6247
|
+
fullStateOnly() {
|
|
6248
|
+
this._fullStateOnly = true;
|
|
6233
6249
|
return this;
|
|
6234
6250
|
}
|
|
6235
6251
|
/**
|
|
@@ -6241,8 +6257,8 @@
|
|
|
6241
6257
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
6242
6258
|
* want on the class for typing convenience without paying any sync cost.
|
|
6243
6259
|
*
|
|
6244
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6245
|
-
* `.unreliable()`, `.
|
|
6260
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
6261
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
6246
6262
|
* them throws at `schema()` time.
|
|
6247
6263
|
*
|
|
6248
6264
|
* ```ts
|
|
@@ -6284,9 +6300,12 @@
|
|
|
6284
6300
|
* higher return values emit first. Does nothing in broadcast mode
|
|
6285
6301
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
6286
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
|
+
*
|
|
6287
6306
|
* ```ts
|
|
6288
6307
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
6289
|
-
* -
|
|
6308
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
6290
6309
|
* )
|
|
6291
6310
|
* ```
|
|
6292
6311
|
*/
|
|
@@ -6321,12 +6340,11 @@
|
|
|
6321
6340
|
default: this._default,
|
|
6322
6341
|
hasDefault: this._hasDefault,
|
|
6323
6342
|
view: this._view,
|
|
6324
|
-
owned: this._owned,
|
|
6325
6343
|
unreliable: this._unreliable,
|
|
6326
|
-
|
|
6344
|
+
patchOnly: this._patchOnly,
|
|
6327
6345
|
deprecated: this._deprecated,
|
|
6328
6346
|
deprecatedThrows: this._deprecatedThrows,
|
|
6329
|
-
|
|
6347
|
+
fullStateOnly: this._fullStateOnly,
|
|
6330
6348
|
stream: this._stream,
|
|
6331
6349
|
optional: this._optional,
|
|
6332
6350
|
noSync: this._noSync,
|
|
@@ -6580,25 +6598,44 @@
|
|
|
6580
6598
|
Metadata.setTag(metadata, fieldName, tag);
|
|
6581
6599
|
};
|
|
6582
6600
|
}
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
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
|
+
*/
|
|
6587
6612
|
function unreliable(target, field) {
|
|
6588
6613
|
const metadata = Metadata.initialize(target.constructor);
|
|
6589
6614
|
Metadata.setUnreliable(metadata, field);
|
|
6590
6615
|
}
|
|
6591
6616
|
/**
|
|
6592
|
-
* @
|
|
6593
|
-
* encodeAllView).
|
|
6617
|
+
* @patchOnly — mark a field as not persisted to snapshots (encodeAll /
|
|
6618
|
+
* encodeAllView). PatchOnly fields are still emitted on per-tick patches
|
|
6594
6619
|
* (reliable or unreliable), but late-joining clients won't see them until
|
|
6595
6620
|
* the next mutation.
|
|
6596
6621
|
*
|
|
6597
6622
|
* Orthogonal to @unreliable: a field can be either, both, or neither.
|
|
6598
6623
|
*/
|
|
6599
|
-
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) {
|
|
6600
6637
|
const metadata = Metadata.initialize(target.constructor);
|
|
6601
|
-
Metadata.
|
|
6638
|
+
Metadata.setFullStateOnly(metadata, field);
|
|
6602
6639
|
}
|
|
6603
6640
|
function type(type, options) {
|
|
6604
6641
|
return function (target, field) {
|
|
@@ -6970,11 +7007,10 @@
|
|
|
6970
7007
|
}
|
|
6971
7008
|
};
|
|
6972
7009
|
const viewTagFields = {};
|
|
6973
|
-
const ownedFields = [];
|
|
6974
7010
|
const unreliableFields = [];
|
|
6975
|
-
const
|
|
7011
|
+
const patchOnlyFields = [];
|
|
6976
7012
|
const deprecatedFields = {};
|
|
6977
|
-
const
|
|
7013
|
+
const fullStateOnlyFields = [];
|
|
6978
7014
|
const streamFields = [];
|
|
6979
7015
|
const streamPriorityFields = {};
|
|
6980
7016
|
const optionalFields = [];
|
|
@@ -6986,15 +7022,22 @@
|
|
|
6986
7022
|
// Local-only field: skip metadata registration entirely so it is
|
|
6987
7023
|
// never encoded/decoded, but still seed its construction default
|
|
6988
7024
|
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6989
|
-
if (def.view !== undefined || def.
|
|
6990
|
-
def.
|
|
7025
|
+
if (def.view !== undefined || def.unreliable ||
|
|
7026
|
+
def.patchOnly || def.fullStateOnly || def.stream) {
|
|
6991
7027
|
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6992
|
-
`together with a sync-only modifier (.view/.
|
|
7028
|
+
`together with a sync-only modifier (.view/.unreliable/.patchOnly/.fullStateOnly/.stream). ` +
|
|
6993
7029
|
`A local-only field cannot be synchronized.`);
|
|
6994
7030
|
}
|
|
6995
7031
|
seedDefault(fieldName, def);
|
|
6996
7032
|
continue;
|
|
6997
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
|
+
}
|
|
6998
7041
|
const normalizedType = getNormalizedType(def.type);
|
|
6999
7042
|
// A synced ref must be encodable (a Schema, or Metadata.setFields()'d) — reject a bare class.
|
|
7000
7043
|
if (typeof normalizedType === "function" && !Schema.is(normalizedType)) {
|
|
@@ -7005,20 +7048,17 @@
|
|
|
7005
7048
|
if (def.view !== undefined) {
|
|
7006
7049
|
viewTagFields[fieldName] = def.view;
|
|
7007
7050
|
}
|
|
7008
|
-
if (def.owned) {
|
|
7009
|
-
ownedFields.push(fieldName);
|
|
7010
|
-
}
|
|
7011
7051
|
if (def.unreliable) {
|
|
7012
7052
|
unreliableFields.push(fieldName);
|
|
7013
7053
|
}
|
|
7014
|
-
if (def.
|
|
7015
|
-
|
|
7054
|
+
if (def.patchOnly) {
|
|
7055
|
+
patchOnlyFields.push(fieldName);
|
|
7016
7056
|
}
|
|
7017
7057
|
if (def.deprecated) {
|
|
7018
7058
|
deprecatedFields[fieldName] = def.deprecatedThrows;
|
|
7019
7059
|
}
|
|
7020
|
-
if (def.
|
|
7021
|
-
|
|
7060
|
+
if (def.fullStateOnly) {
|
|
7061
|
+
fullStateOnlyFields.push(fieldName);
|
|
7022
7062
|
}
|
|
7023
7063
|
if (def.stream) {
|
|
7024
7064
|
streamFields.push(fieldName);
|
|
@@ -7101,22 +7141,19 @@
|
|
|
7101
7141
|
for (const fieldName in viewTagFields) {
|
|
7102
7142
|
view(viewTagFields[fieldName])(klass.prototype, fieldName);
|
|
7103
7143
|
}
|
|
7104
|
-
for (const fieldName of ownedFields) {
|
|
7105
|
-
owned(klass.prototype, fieldName);
|
|
7106
|
-
}
|
|
7107
7144
|
for (const fieldName of unreliableFields) {
|
|
7108
7145
|
unreliable(klass.prototype, fieldName);
|
|
7109
7146
|
}
|
|
7110
|
-
for (const fieldName of
|
|
7111
|
-
|
|
7147
|
+
for (const fieldName of patchOnlyFields) {
|
|
7148
|
+
patchOnly(klass.prototype, fieldName);
|
|
7112
7149
|
}
|
|
7113
7150
|
for (const fieldName in deprecatedFields) {
|
|
7114
7151
|
deprecated(deprecatedFields[fieldName])(klass.prototype, fieldName);
|
|
7115
7152
|
}
|
|
7116
|
-
if (
|
|
7153
|
+
if (fullStateOnlyFields.length > 0 || streamFields.length > 0) {
|
|
7117
7154
|
const metadata = klass[Symbol.metadata];
|
|
7118
|
-
for (const fieldName of
|
|
7119
|
-
Metadata.
|
|
7155
|
+
for (const fieldName of fullStateOnlyFields) {
|
|
7156
|
+
Metadata.setFullStateOnly(metadata, fieldName);
|
|
7120
7157
|
}
|
|
7121
7158
|
for (const fieldName of streamFields) {
|
|
7122
7159
|
Metadata.setStream(metadata, fieldName);
|
|
@@ -7765,7 +7802,7 @@
|
|
|
7765
7802
|
const previousRefCount = this.refCount[refId];
|
|
7766
7803
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
7767
7804
|
//
|
|
7768
|
-
// Re-stage every currently-populated non-
|
|
7805
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
7769
7806
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
7770
7807
|
// re-emits it on the correct channel. Two triggers:
|
|
7771
7808
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -7855,14 +7892,10 @@
|
|
|
7855
7892
|
const parentNode = parent[$changes][nodeField];
|
|
7856
7893
|
if (!parentNode || parentNode === node)
|
|
7857
7894
|
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
|
|
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;
|
|
7866
7899
|
// Remove node from current position
|
|
7867
7900
|
if (node.prev) {
|
|
7868
7901
|
node.prev.next = node.next;
|
|
@@ -7876,16 +7909,18 @@
|
|
|
7876
7909
|
else {
|
|
7877
7910
|
changeSet.tail = node.prev;
|
|
7878
7911
|
}
|
|
7879
|
-
//
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
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++;
|
|
7889
7924
|
}
|
|
7890
7925
|
enqueueChangeTree(changeTree, existingNode = changeTree.changesNode) {
|
|
7891
7926
|
if (existingNode) {
|
|
@@ -7907,12 +7942,12 @@
|
|
|
7907
7942
|
node.changeTree = changeTree;
|
|
7908
7943
|
node.next = undefined;
|
|
7909
7944
|
node.prev = undefined;
|
|
7910
|
-
node.position = 0;
|
|
7911
7945
|
}
|
|
7912
7946
|
else {
|
|
7913
7947
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
7914
7948
|
}
|
|
7915
7949
|
if (!list.next) {
|
|
7950
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
7916
7951
|
list.next = node;
|
|
7917
7952
|
list.tail = node;
|
|
7918
7953
|
}
|
|
@@ -7921,6 +7956,7 @@
|
|
|
7921
7956
|
list.tail.next = node;
|
|
7922
7957
|
list.tail = node;
|
|
7923
7958
|
}
|
|
7959
|
+
node.position = list.nextPosition++;
|
|
7924
7960
|
return node;
|
|
7925
7961
|
}
|
|
7926
7962
|
/**
|
|
@@ -8019,6 +8055,7 @@
|
|
|
8019
8055
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8020
8056
|
ctx.isSchema = desc.isSchema;
|
|
8021
8057
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8058
|
+
ctx.tags = desc.tags;
|
|
8022
8059
|
ctx.structSwitchEmitted = false;
|
|
8023
8060
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
8024
8061
|
// Call the module function directly — the `forEachLiveWithCtx`
|
|
@@ -8058,10 +8095,13 @@
|
|
|
8058
8095
|
}
|
|
8059
8096
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
8060
8097
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
8061
|
-
// itself carries a @view tag.
|
|
8062
|
-
//
|
|
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.
|
|
8063
8101
|
const fieldFiltered = ctx.isSchema
|
|
8064
|
-
? (ctx.treeIsFiltered || (
|
|
8102
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
8103
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
8104
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
8065
8105
|
: ctx.treeIsFiltered;
|
|
8066
8106
|
if (fieldFiltered !== ctx.emitFiltered)
|
|
8067
8107
|
return;
|
|
@@ -8116,7 +8156,7 @@
|
|
|
8116
8156
|
ref: undefined, encoder: undefined, filter: undefined, metadata: undefined,
|
|
8117
8157
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
8118
8158
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
8119
|
-
filterBitmask: 0,
|
|
8159
|
+
filterBitmask: 0, tags: undefined,
|
|
8120
8160
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
8121
8161
|
gen: 0, initialOffset: 0, rootChangeTree: undefined,
|
|
8122
8162
|
};
|
|
@@ -8173,6 +8213,7 @@
|
|
|
8173
8213
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
8174
8214
|
ctx.isSchema = desc.isSchema;
|
|
8175
8215
|
ctx.filterBitmask = desc.filterBitmask;
|
|
8216
|
+
ctx.tags = desc.tags;
|
|
8176
8217
|
ctx.structSwitchEmitted = false;
|
|
8177
8218
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
8178
8219
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -8424,7 +8465,7 @@
|
|
|
8424
8465
|
// Emit each element's full state — forEachLive walks populated
|
|
8425
8466
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
8426
8467
|
// Covers both static elements (dirty state was reset by
|
|
8427
|
-
// inheritedFlags'
|
|
8468
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
8428
8469
|
// has dirty state but the main loop skipped them because
|
|
8429
8470
|
// they're filtered).
|
|
8430
8471
|
for (const element of emittedElements) {
|
|
@@ -8503,20 +8544,81 @@
|
|
|
8503
8544
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
8504
8545
|
// into `_stream.priority` when the stream was attached. Users
|
|
8505
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;
|
|
8506
8552
|
const priority = st.priority;
|
|
8507
|
-
|
|
8508
|
-
//
|
|
8509
|
-
//
|
|
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.
|
|
8510
8566
|
const positions = [];
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
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]);
|
|
8517
8610
|
}
|
|
8518
|
-
|
|
8519
|
-
|
|
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;
|
|
8520
8622
|
let sent = st.sentByView.get(viewId);
|
|
8521
8623
|
if (sent === undefined) {
|
|
8522
8624
|
sent = new Set();
|
|
@@ -9871,6 +9973,31 @@
|
|
|
9871
9973
|
_clearViewBitFromAllTrees(root, slot, bit);
|
|
9872
9974
|
root.releaseViewId(id);
|
|
9873
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
|
+
}
|
|
9874
10001
|
class StateView {
|
|
9875
10002
|
iterable;
|
|
9876
10003
|
/**
|
|
@@ -10095,12 +10222,12 @@
|
|
|
10095
10222
|
}
|
|
10096
10223
|
_add(obj, tag, checkIncludeParent, _skipStreamRouting) {
|
|
10097
10224
|
const changeTree = obj?.[$changes];
|
|
10098
|
-
const parentChangeTree = changeTree.parent;
|
|
10099
10225
|
if (!changeTree) {
|
|
10100
|
-
console.warn(
|
|
10226
|
+
console.warn(`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10101
10227
|
return false;
|
|
10102
10228
|
}
|
|
10103
|
-
|
|
10229
|
+
const parentChangeTree = changeTree.parent;
|
|
10230
|
+
if (!parentChangeTree &&
|
|
10104
10231
|
obj[$refId] !== 0 // allow root object
|
|
10105
10232
|
) {
|
|
10106
10233
|
/**
|
|
@@ -10381,9 +10508,9 @@
|
|
|
10381
10508
|
}
|
|
10382
10509
|
}
|
|
10383
10510
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
10384
|
-
const changeTree = obj[$changes];
|
|
10511
|
+
const changeTree = obj?.[$changes];
|
|
10385
10512
|
if (!changeTree) {
|
|
10386
|
-
console.warn(
|
|
10513
|
+
console.warn(`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`);
|
|
10387
10514
|
return this;
|
|
10388
10515
|
}
|
|
10389
10516
|
// ── Streamable-element unsubscribe ─────────────────────────────
|
|
@@ -10521,32 +10648,47 @@
|
|
|
10521
10648
|
hasTag(ob, tag = DEFAULT_VIEW_TAG) {
|
|
10522
10649
|
return this.hasTagOnTree(ob[$changes], tag);
|
|
10523
10650
|
}
|
|
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) {
|
|
10651
|
+
subscribe(collection, priority) {
|
|
10542
10652
|
const tree = collection?.[$changes];
|
|
10543
10653
|
if (!tree) {
|
|
10544
|
-
console.warn(
|
|
10654
|
+
console.warn(`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10545
10655
|
return this;
|
|
10546
10656
|
}
|
|
10547
10657
|
if (this._root === undefined && tree.root !== undefined) {
|
|
10548
10658
|
this._bindRoot(tree.root);
|
|
10549
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
|
+
}
|
|
10550
10692
|
if (this.isSubscribed(tree))
|
|
10551
10693
|
return this;
|
|
10552
10694
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -10592,7 +10734,7 @@
|
|
|
10592
10734
|
unsubscribe(collection) {
|
|
10593
10735
|
const tree = collection?.[$changes];
|
|
10594
10736
|
if (!tree) {
|
|
10595
|
-
console.warn(
|
|
10737
|
+
console.warn(`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`);
|
|
10596
10738
|
return this;
|
|
10597
10739
|
}
|
|
10598
10740
|
if (!this.isSubscribed(tree))
|
|
@@ -10721,15 +10863,15 @@
|
|
|
10721
10863
|
exports.encodeMapEntry = encodeMapEntry;
|
|
10722
10864
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
10723
10865
|
exports.entity = entity;
|
|
10866
|
+
exports.fullStateOnly = fullStateOnly;
|
|
10724
10867
|
exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
|
|
10725
10868
|
exports.getEncodeDescriptor = getEncodeDescriptor;
|
|
10726
10869
|
exports.getRawChangesCallback = getRawChangesCallback;
|
|
10727
10870
|
exports.isBuilder = isBuilder;
|
|
10728
|
-
exports.
|
|
10871
|
+
exports.patchOnly = patchOnly;
|
|
10729
10872
|
exports.registerType = registerType;
|
|
10730
10873
|
exports.schema = schema;
|
|
10731
10874
|
exports.t = t;
|
|
10732
|
-
exports.transient = transient;
|
|
10733
10875
|
exports.type = type;
|
|
10734
10876
|
exports.unreliable = unreliable;
|
|
10735
10877
|
exports.view = view;
|