@colyseus/schema 5.0.11 → 5.0.13

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