@irtio/schema 4.7.1 → 4.8.0

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 (3) hide show
  1. package/dist/index.d.ts +124 -10
  2. package/dist/index.js +708 -62
  3. package/package.json +4 -1
package/dist/index.js CHANGED
@@ -13,7 +13,8 @@ var TypeImpl = class _TypeImpl {
13
13
  }
14
14
  const err = validateValue(this.desc, value);
15
15
  if (err) throw new Error(`invalid default for ${describeType(this.desc)}: ${err}`);
16
- return new _TypeImpl({ ...this.desc, hasDefault: true, default: value });
16
+ const stored = value instanceof Uint8Array ? copyBytes(value) : value;
17
+ return new _TypeImpl({ ...this.desc, hasDefault: true, default: stored });
17
18
  }
18
19
  };
19
20
  function make(desc) {
@@ -39,8 +40,32 @@ function enumOf(...values) {
39
40
  function ref(entity2) {
40
41
  return make({ kind: "ref", entity: entity2 });
41
42
  }
43
+ var BYTES_MAX = 65535;
44
+ function bytes(n) {
45
+ if (!Number.isInteger(n) || n < 1 || n > BYTES_MAX) {
46
+ throw new Error(`bytes(n): n must be an integer from 1 to ${BYTES_MAX}, got ${String(n)}`);
47
+ }
48
+ return make({ kind: "bytes", n });
49
+ }
50
+ function containsBytes(d) {
51
+ switch (d.kind) {
52
+ case "bytes":
53
+ return true;
54
+ case "list":
55
+ return containsBytes(d.item);
56
+ case "struct":
57
+ return Object.values(d.fields).some(containsBytes);
58
+ default:
59
+ return false;
60
+ }
61
+ }
42
62
  function list(item, max) {
43
63
  assertMax(max, "list");
64
+ if (containsBytes(item.desc)) {
65
+ throw new Error(
66
+ "list items may not contain bytes(n) \u2014 a list is whole-replace; model chunks as an entity collection"
67
+ );
68
+ }
44
69
  return make({ kind: "list", item: item.desc, max });
45
70
  }
46
71
  function struct(fields) {
@@ -73,6 +98,9 @@ function describeType(d) {
73
98
  case "ref":
74
99
  s = `ref(${JSON.stringify(d.entity)})`;
75
100
  break;
101
+ case "bytes":
102
+ s = `bytes(${d.n})`;
103
+ break;
76
104
  case "list":
77
105
  s = `list(${describeType(d.item)}, ${d.max})`;
78
106
  break;
@@ -123,6 +151,8 @@ function validateValue(d, v, path = "") {
123
151
  }
124
152
  case "enum":
125
153
  return typeof v === "string" && d.values.includes(v) ? null : `${at} must be one of ${d.values.join("|")}`;
154
+ case "bytes":
155
+ return v instanceof Uint8Array && v.length === d.n ? null : `${at} must be a Uint8Array of length ${d.n} (got ${describeBytesValue(v)})`;
126
156
  case "list": {
127
157
  if (!Array.isArray(v)) return `${at} must be an array`;
128
158
  if (v.length > d.max) return `${at} exceeds max length ${d.max} (${v.length})`;
@@ -160,6 +190,8 @@ function defaultValue(d) {
160
190
  return "";
161
191
  case "enum":
162
192
  return d.values[0];
193
+ case "bytes":
194
+ return new Uint8Array(d.n);
163
195
  case "list":
164
196
  return [];
165
197
  case "struct": {
@@ -170,6 +202,7 @@ function defaultValue(d) {
170
202
  }
171
203
  }
172
204
  function cloneValue(v) {
205
+ if (v instanceof Uint8Array) return copyBytes(v);
173
206
  if (Array.isArray(v)) return v.map(cloneValue);
174
207
  if (v !== null && typeof v === "object") {
175
208
  const o = {};
@@ -199,6 +232,11 @@ function normalizeValue(d, v, path = "") {
199
232
  if (v.length > d.max) throw new Error(`${at} exceeds max length ${d.max} (${v.length})`);
200
233
  return v.map((x, i) => normalizeValue(d.item, x, `${at}[${i}]`));
201
234
  }
235
+ case "bytes": {
236
+ const err = validateValue(d, v, path);
237
+ if (err) throw new Error(err);
238
+ return copyBytes(v);
239
+ }
202
240
  case "struct": {
203
241
  if (typeof v !== "object" || v === null || Array.isArray(v))
204
242
  throw new Error(`${at} must be an object`);
@@ -215,6 +253,83 @@ function normalizeValue(d, v, path = "") {
215
253
  }
216
254
  }
217
255
  }
256
+ function copyBytes(v) {
257
+ return ArrayBuffer.isView(v) ? new Uint8Array(v) : v.slice();
258
+ }
259
+ function describeBytesValue(v) {
260
+ if (v instanceof Uint8Array) return `a Uint8Array of length ${v.length}`;
261
+ if (Array.isArray(v)) return `an array of length ${v.length}`;
262
+ if (v === null) return "null";
263
+ if (ArrayBuffer.isView(v)) return `a ${v.constructor.name}`;
264
+ return typeof v;
265
+ }
266
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
267
+ function bytesToBase64(b) {
268
+ let out = "";
269
+ let i = 0;
270
+ for (; i + 2 < b.length; i += 3) {
271
+ const x = b[i] << 16 | b[i + 1] << 8 | b[i + 2];
272
+ out += B64[x >> 18] + B64[x >> 12 & 63] + B64[x >> 6 & 63] + B64[x & 63];
273
+ }
274
+ const rest = b.length - i;
275
+ if (rest === 1) {
276
+ const x = b[i] << 16;
277
+ out += `${B64[x >> 18]}${B64[x >> 12 & 63]}==`;
278
+ } else if (rest === 2) {
279
+ const x = b[i] << 16 | b[i + 1] << 8;
280
+ out += `${B64[x >> 18]}${B64[x >> 12 & 63]}${B64[x >> 6 & 63]}=`;
281
+ }
282
+ return out;
283
+ }
284
+ function base64ToBytes(s) {
285
+ if (s.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(s)) {
286
+ throw new Error("invalid base64");
287
+ }
288
+ const pad = s.endsWith("==") ? 2 : s.endsWith("=") ? 1 : 0;
289
+ const out = new Uint8Array(s.length / 4 * 3 - pad);
290
+ let o = 0;
291
+ for (let i = 0; i < s.length; i += 4) {
292
+ const x = B64.indexOf(s[i]) << 18 | B64.indexOf(s[i + 1]) << 12 | (s[i + 2] === "=" ? 0 : B64.indexOf(s[i + 2])) << 6 | (s[i + 3] === "=" ? 0 : B64.indexOf(s[i + 3]));
293
+ out[o++] = x >> 16;
294
+ if (o < out.length) out[o++] = x >> 8 & 255;
295
+ if (o < out.length) out[o++] = x & 255;
296
+ }
297
+ return out;
298
+ }
299
+ function jsonDefault(d, v) {
300
+ if (v === void 0 || v === null) return v;
301
+ switch (d.kind) {
302
+ case "bytes":
303
+ return v instanceof Uint8Array ? bytesToBase64(v) : v;
304
+ case "struct": {
305
+ const o = {};
306
+ for (const [k, fd] of Object.entries(d.fields)) {
307
+ o[k] = jsonDefault(fd, v[k]);
308
+ }
309
+ return o;
310
+ }
311
+ default:
312
+ return v;
313
+ }
314
+ }
315
+ function defaultFromJson(d, v) {
316
+ if (v === void 0 || v === null) return v;
317
+ switch (d.kind) {
318
+ case "bytes":
319
+ return typeof v === "string" ? base64ToBytes(v) : v;
320
+ case "struct": {
321
+ if (typeof v !== "object") return v;
322
+ const o = {};
323
+ for (const [k, fd] of Object.entries(d.fields)) {
324
+ const x = v[k];
325
+ if (x !== void 0) o[k] = defaultFromJson(fd, x);
326
+ }
327
+ return o;
328
+ }
329
+ default:
330
+ return v;
331
+ }
332
+ }
218
333
 
219
334
  // src/physics.ts
220
335
  var PHYSICS_BODY_CHANNELS = [
@@ -647,9 +762,9 @@ function sha256(data) {
647
762
  function sha256Hex(data) {
648
763
  return toHex(sha256(data));
649
764
  }
650
- function toHex(bytes) {
765
+ function toHex(bytes2) {
651
766
  let s = "";
652
- for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, "0");
767
+ for (let i = 0; i < bytes2.length; i++) s += bytes2[i].toString(16).padStart(2, "0");
653
768
  return s;
654
769
  }
655
770
 
@@ -715,13 +830,13 @@ function maxEncodedBytes(fields, names) {
715
830
  return total;
716
831
  }
717
832
  function varintBytes(max) {
718
- let bytes = 1;
833
+ let bytes2 = 1;
719
834
  let v = Math.max(0, Math.floor(max));
720
835
  while (v > 127) {
721
836
  v = Math.floor(v / 128);
722
- bytes++;
837
+ bytes2++;
723
838
  }
724
- return bytes;
839
+ return bytes2;
725
840
  }
726
841
  function worstCase(d) {
727
842
  const presence = d.opt ? 1 : 0;
@@ -742,6 +857,8 @@ function worstCase(d) {
742
857
  return presence + varintBytes(d.max) + d.max;
743
858
  case "ref":
744
859
  return presence + varintBytes(ID_MAX_BYTES) + ID_MAX_BYTES;
860
+ case "bytes":
861
+ return presence + d.n;
745
862
  case "list":
746
863
  return presence + varintBytes(d.max) + d.max * worstCase(d.item);
747
864
  case "struct": {
@@ -778,12 +895,17 @@ function compilePersist(name, fields, perPlayer, declared) {
778
895
  `${name}: persist names ${field}, an entity ref \u2014 a ref is an id inside one room and means nothing in the next one`
779
896
  );
780
897
  }
898
+ if (containsBytes(known.get(field).type)) {
899
+ throw new Error(
900
+ `${name}: persist names ${field}, a bytes field \u2014 the persistence blob is JSON and does not carry binary`
901
+ );
902
+ }
781
903
  out.push(field);
782
904
  }
783
- const bytes = maxEncodedBytes(fields, out);
784
- if (bytes > PERSIST_BLOB_BYTES) {
905
+ const bytes2 = maxEncodedBytes(fields, out);
906
+ if (bytes2 > PERSIST_BLOB_BYTES) {
785
907
  throw new Error(
786
- `${name}: persist fields can encode up to ${bytes} bytes, over the 4 KB persistence cap`
908
+ `${name}: persist fields can encode up to ${bytes2} bytes, over the 4 KB persistence cap`
787
909
  );
788
910
  }
789
911
  return out.sort();
@@ -848,6 +970,7 @@ function defineSchema(defs, options = {}) {
848
970
  const ownership = o.ownership || perPlayer ? compileOwnership(name, def.kind, fields, o.ownership ?? {}, perPlayer) : void 0;
849
971
  const persist = compilePersist(name, fields, perPlayer, o.persist);
850
972
  const serverFields = compileServerFields(name, def.kind, fields, o);
973
+ checkBytesWriters(name, def.kind, fields, o, serverFields);
851
974
  return {
852
975
  name,
853
976
  index,
@@ -988,6 +1111,15 @@ function defineSchema(defs, options = {}) {
988
1111
  hash8: hashBytes.slice(0, 8)
989
1112
  };
990
1113
  }
1114
+ function checkBytesWriters(name, kind, fields, o, serverFields) {
1115
+ if (kind !== "entity" || o.serverOwned === true) return;
1116
+ for (const f of fields) {
1117
+ if (!containsBytes(f.type) || serverFields?.has(f.name)) continue;
1118
+ throw new Error(
1119
+ `${name}.${f.name}: bytes(n) fields are server-written only in this release \u2014 declare the collection serverOwned: true or list ${JSON.stringify(f.name)} in serverFields`
1120
+ );
1121
+ }
1122
+ }
991
1123
  function compileFields(fields) {
992
1124
  return Object.entries(fields).map(([name, t], index) => ({ name, index, type: t.desc }));
993
1125
  }
@@ -1011,7 +1143,7 @@ var CANONICAL_VERSION = 1;
1011
1143
  function canonicalType(d) {
1012
1144
  const o = { kind: d.kind };
1013
1145
  if (d.opt) o.opt = true;
1014
- if (d.hasDefault) o.default = d.default;
1146
+ if (d.hasDefault) o.default = containsBytes(d) ? jsonDefault(d, d.default) : d.default;
1015
1147
  switch (d.kind) {
1016
1148
  case "str":
1017
1149
  o.max = d.max;
@@ -1022,6 +1154,9 @@ function canonicalType(d) {
1022
1154
  case "ref":
1023
1155
  o.entity = d.entity;
1024
1156
  break;
1157
+ case "bytes":
1158
+ o.n = d.n;
1159
+ break;
1025
1160
  case "list":
1026
1161
  o.item = canonicalType(d.item);
1027
1162
  o.max = d.max;
@@ -1332,6 +1467,9 @@ function typeOf(t, at) {
1332
1467
  case "ref":
1333
1468
  base = ref(stringAt(t, "entity"));
1334
1469
  break;
1470
+ case "bytes":
1471
+ base = bytes(numberAt(t, "n"));
1472
+ break;
1335
1473
  case "list":
1336
1474
  base = list(typeOf(objectAt(t, "item"), `${at}[]`), numberAt(t, "max"));
1337
1475
  break;
@@ -1342,7 +1480,10 @@ function typeOf(t, at) {
1342
1480
  throw new Error(`${at}: unknown type kind ${JSON.stringify(kind)}`);
1343
1481
  }
1344
1482
  if (t.opt === true) base = base.opt;
1345
- if (Object.hasOwn(t, "default")) base = base.default(t.default);
1483
+ if (Object.hasOwn(t, "default")) {
1484
+ const value = containsBytes(base.desc) ? defaultFromJson(base.desc, t.default) : t.default;
1485
+ base = base.default(value);
1486
+ }
1346
1487
  return base;
1347
1488
  }
1348
1489
  function entityOptions(e, name) {
@@ -1565,6 +1706,7 @@ function createState(schema, init) {
1565
1706
  }
1566
1707
 
1567
1708
  // src/dirty.ts
1709
+ var MAX_BYTE_RANGES = 16;
1568
1710
  function createDirtySet() {
1569
1711
  return /* @__PURE__ */ new Map();
1570
1712
  }
@@ -1610,6 +1752,87 @@ function markPath(mask, path) {
1610
1752
  const last = path[path.length - 1];
1611
1753
  m.fields.add(last);
1612
1754
  m.nested.delete(last);
1755
+ m.ranges?.delete(last);
1756
+ }
1757
+ function markBytes(dirty, collection, id, path, start, end, n) {
1758
+ const r = recordDirty(dirty, collection, id);
1759
+ if (r) markBytesPath(r.mask, path, start, end, n);
1760
+ }
1761
+ function markBytesPath(mask, path, start, end, n) {
1762
+ if (path.length === 0 || end <= start) return;
1763
+ let m = mask;
1764
+ for (let i = 0; i < path.length - 1; i++) {
1765
+ const idx = path[i];
1766
+ if (isWhole(m, idx)) return;
1767
+ m.fields.add(idx);
1768
+ let nm = m.nested.get(idx);
1769
+ if (!nm) {
1770
+ nm = createFieldMask();
1771
+ m.nested.set(idx, nm);
1772
+ }
1773
+ m = nm;
1774
+ }
1775
+ const last = path[path.length - 1];
1776
+ if (m.fields.has(last) && !m.ranges?.has(last)) return;
1777
+ m.fields.add(last);
1778
+ const ranges = m.ranges ??= /* @__PURE__ */ new Map();
1779
+ const merged = unionRanges(ranges.get(last) ?? [], [{ start, end }]);
1780
+ const settled = settleRanges(merged, n);
1781
+ if (settled) ranges.set(last, settled);
1782
+ else ranges.delete(last);
1783
+ }
1784
+ function varintSize(v) {
1785
+ let n = 1;
1786
+ let x = v;
1787
+ while (x >= 128) {
1788
+ x = Math.floor(x / 128);
1789
+ n++;
1790
+ }
1791
+ return n;
1792
+ }
1793
+ function rangesPatchBytes(ranges) {
1794
+ let total = varintSize(ranges.length);
1795
+ for (const r of ranges) {
1796
+ const len = r.end - r.start;
1797
+ total += varintSize(r.start) + varintSize(len) + len;
1798
+ }
1799
+ return total;
1800
+ }
1801
+ function settleRanges(ranges, n) {
1802
+ if (ranges.length === 0 || ranges.length > MAX_BYTE_RANGES) return void 0;
1803
+ if (n !== void 0 && rangesPatchBytes(ranges) >= n) return void 0;
1804
+ return ranges;
1805
+ }
1806
+ function unionRanges(a, b) {
1807
+ const all = [...a, ...b].sort((x, y) => x.start - y.start);
1808
+ const out = [];
1809
+ for (const r of all) {
1810
+ const last = out[out.length - 1];
1811
+ if (last && last.end >= r.start) {
1812
+ if (r.end > last.end) out[out.length - 1] = { start: last.start, end: r.end };
1813
+ } else {
1814
+ out.push({ start: r.start, end: r.end });
1815
+ }
1816
+ }
1817
+ return out;
1818
+ }
1819
+ function diffByteRanges(a, b, gap = 4) {
1820
+ const out = [];
1821
+ const n = Math.min(a.length, b.length);
1822
+ let i = 0;
1823
+ while (i < n) {
1824
+ if (a[i] === b[i]) {
1825
+ i++;
1826
+ continue;
1827
+ }
1828
+ let end = i + 1;
1829
+ while (end < n && a[end] !== b[end]) end++;
1830
+ const last = out[out.length - 1];
1831
+ if (last && i - last.end < gap) out[out.length - 1] = { start: last.start, end };
1832
+ else out.push({ start: i, end });
1833
+ i = end;
1834
+ }
1835
+ return out;
1613
1836
  }
1614
1837
  function markOwner(dirty, collection, id) {
1615
1838
  const r = recordDirty(dirty, collection, id);
@@ -1634,7 +1857,7 @@ function isDirtyEmpty(dirty) {
1634
1857
  return true;
1635
1858
  }
1636
1859
  function isWhole(mask, idx) {
1637
- return mask.fields.has(idx) && !mask.nested.has(idx);
1860
+ return mask.fields.has(idx) && !mask.nested.has(idx) && !mask.ranges?.has(idx);
1638
1861
  }
1639
1862
  function mergeDirty(dst, src) {
1640
1863
  for (const [name, sc] of src) {
@@ -1651,9 +1874,18 @@ function mergeDirty(dst, src) {
1651
1874
  function mergeMask(dst, src) {
1652
1875
  for (const idx of src.fields) {
1653
1876
  const n = src.nested.get(idx);
1654
- if (!n) {
1877
+ const sr = src.ranges?.get(idx);
1878
+ if (sr) {
1879
+ if (dst.fields.has(idx) && !dst.ranges?.has(idx)) continue;
1880
+ dst.fields.add(idx);
1881
+ const ranges = dst.ranges ??= /* @__PURE__ */ new Map();
1882
+ const settled = settleRanges(unionRanges(ranges.get(idx) ?? [], sr));
1883
+ if (settled) ranges.set(idx, settled);
1884
+ else ranges.delete(idx);
1885
+ } else if (!n) {
1655
1886
  dst.fields.add(idx);
1656
1887
  dst.nested.delete(idx);
1888
+ dst.ranges?.delete(idx);
1657
1889
  } else if (!isWhole(dst, idx)) {
1658
1890
  dst.fields.add(idx);
1659
1891
  let dn = dst.nested.get(idx);
@@ -1773,11 +2005,11 @@ var ByteWriter = class {
1773
2005
  return this.buf.slice(0, this.pos);
1774
2006
  }
1775
2007
  };
1776
- function utf82(bytes) {
2008
+ function utf82(bytes2) {
1777
2009
  try {
1778
- return decoder.decode(bytes);
2010
+ return decoder.decode(bytes2);
1779
2011
  } catch {
1780
- throw new Error(`invalid UTF-8 in string (${bytes.length} bytes)`);
2012
+ throw new Error(`invalid UTF-8 in string (${bytes2.length} bytes)`);
1781
2013
  }
1782
2014
  }
1783
2015
  var ByteReader = class {
@@ -1889,7 +2121,7 @@ function bytesEqual(a, b) {
1889
2121
  }
1890
2122
 
1891
2123
  // src/codec.ts
1892
- function varintSize(v) {
2124
+ function varintSize2(v) {
1893
2125
  let n = 1;
1894
2126
  let x = v;
1895
2127
  while (x >= 128) {
@@ -1924,14 +2156,14 @@ var CountingSink = class {
1924
2156
  varint(v) {
1925
2157
  if (!Number.isInteger(v) || v < 0 || v > 4294967295)
1926
2158
  throw new Error(`varint out of range: ${v}`);
1927
- this.length += varintSize(v);
2159
+ this.length += varintSize2(v);
1928
2160
  }
1929
2161
  bytes(b) {
1930
2162
  this.length += b.length;
1931
2163
  }
1932
2164
  str(s) {
1933
2165
  const n = utf8Length(s);
1934
- this.length += varintSize(n) + n;
2166
+ this.length += varintSize2(n) + n;
1935
2167
  return n;
1936
2168
  }
1937
2169
  };
@@ -2000,6 +2232,9 @@ function writeValue(w, desc, v, path = "value") {
2000
2232
  w.u8(i);
2001
2233
  return;
2002
2234
  }
2235
+ case "bytes":
2236
+ w.bytes(checkBytes(value, desc.n, path));
2237
+ return;
2003
2238
  case "list": {
2004
2239
  if (!Array.isArray(value)) fail(path, "must be an array");
2005
2240
  if (value.length > desc.max) fail(path, `exceeds max length ${desc.max} (${value.length})`);
@@ -2016,6 +2251,14 @@ function writeValue(w, desc, v, path = "value") {
2016
2251
  }
2017
2252
  }
2018
2253
  }
2254
+ function checkBytes(value, n, path) {
2255
+ if (!(value instanceof Uint8Array) || value.length !== n) {
2256
+ fail(path, `must be a Uint8Array of length ${n} (got ${describeBytesValue(value)})`);
2257
+ }
2258
+ return value;
2259
+ }
2260
+ var BYTES_WHOLE = 0;
2261
+ var BYTES_PATCH = 1;
2019
2262
  function warnNonFinite(path, value) {
2020
2263
  console.warn(
2021
2264
  `irtio: snapshot field ${path} held ${value}; restored as the field's default. The value predates the finiteness checks on write and encode.`
@@ -2054,6 +2297,8 @@ function readValue(r, desc, opts, path = "value") {
2054
2297
  throw new Error(`unknown enum index ${i} (${desc.values.length} values)`);
2055
2298
  return v;
2056
2299
  }
2300
+ case "bytes":
2301
+ return copyBytes(r.bytes(desc.n));
2057
2302
  case "list": {
2058
2303
  const n = r.varint();
2059
2304
  if (n > desc.max) throw new Error(`list exceeds max length ${desc.max} (${n})`);
@@ -2085,8 +2330,8 @@ function encodeFields(fields, value) {
2085
2330
  writeRecord(w, fields, value, "fields");
2086
2331
  return w.finish();
2087
2332
  }
2088
- function decodeFields(fields, bytes) {
2089
- const r = bytes instanceof ByteReader ? bytes : new ByteReader(bytes);
2333
+ function decodeFields(fields, bytes2) {
2334
+ const r = bytes2 instanceof ByteReader ? bytes2 : new ByteReader(bytes2);
2090
2335
  return readRecord(r, fields);
2091
2336
  }
2092
2337
  function entityOf(state, name) {
@@ -2137,8 +2382,8 @@ function encodeSnapshot(schema, state, header, options) {
2137
2382
  writeSnapshot(w, schema, state, header.tick, options);
2138
2383
  return w.finish();
2139
2384
  }
2140
- function decodeSnapshot(schema, bytes, options = {}) {
2141
- const r = new ByteReader(bytes);
2385
+ function decodeSnapshot(schema, bytes2, options = {}) {
2386
+ const r = new ByteReader(bytes2);
2142
2387
  const opts = { onNonFinite: options.onNonFinite ?? warnNonFinite };
2143
2388
  const tick = r.u32();
2144
2389
  const hash8 = r.bytes(8).slice();
@@ -2182,14 +2427,18 @@ function planCollection(c, state, dirty) {
2182
2427
  return ops.length > 0 ? ops : void 0;
2183
2428
  }
2184
2429
  function maskBytesFor(indices, bits) {
2185
- const bytes = new Uint8Array(bits + 7 >> 3);
2186
- for (const i of indices) bytes[i >> 3] |= 1 << (i & 7);
2187
- return bytes;
2430
+ const bytes2 = new Uint8Array(bits + 7 >> 3);
2431
+ for (const i of indices) bytes2[i >> 3] |= 1 << (i & 7);
2432
+ return bytes2;
2188
2433
  }
2189
2434
  function sortedIndices(mask, limit) {
2190
2435
  return [...mask.fields].filter((i) => i >= 0 && i < limit).sort((a, b) => a - b);
2191
2436
  }
2192
- function writeDirtyField(w, desc, v, nested, path) {
2437
+ function writeDirtyField(w, desc, v, nested, ranges, path) {
2438
+ if (desc.kind === "bytes") {
2439
+ writeDirtyBytes(w, desc, v, ranges, path);
2440
+ return;
2441
+ }
2193
2442
  if (desc.kind !== "struct") {
2194
2443
  writeValue(w, desc, v, path);
2195
2444
  return;
@@ -2214,8 +2463,41 @@ function writeDirtyField(w, desc, v, nested, path) {
2214
2463
  for (const i of indices) {
2215
2464
  const name = names[i];
2216
2465
  const fd = desc.fields[name];
2217
- writeDirtyField(w, fd, o[name], nested?.nested.get(i), `${path}.${name}`);
2466
+ writeDirtyField(
2467
+ w,
2468
+ fd,
2469
+ o[name],
2470
+ nested?.nested.get(i),
2471
+ nested?.ranges?.get(i),
2472
+ `${path}.${name}`
2473
+ );
2474
+ }
2475
+ }
2476
+ function writeDirtyBytes(w, desc, v, ranges, path) {
2477
+ let value = v;
2478
+ if (value === void 0 && desc.hasDefault && desc.default !== void 0) value = desc.default;
2479
+ if (desc.opt) {
2480
+ if (value === void 0) {
2481
+ w.u8(0);
2482
+ return;
2483
+ }
2484
+ w.u8(1);
2485
+ } else if (value === void 0) {
2486
+ fail(path, "is required");
2487
+ }
2488
+ const b = checkBytes(value, desc.n, path);
2489
+ if (ranges && ranges.length > 0 && ranges.length <= MAX_BYTE_RANGES && rangesPatchBytes(ranges) < desc.n) {
2490
+ w.u8(BYTES_PATCH);
2491
+ w.varint(ranges.length);
2492
+ for (const r of ranges) {
2493
+ w.varint(r.start);
2494
+ w.varint(r.end - r.start);
2495
+ w.bytes(b.subarray(r.start, r.end));
2496
+ }
2497
+ return;
2218
2498
  }
2499
+ w.u8(BYTES_WHOLE);
2500
+ w.bytes(b);
2219
2501
  }
2220
2502
  function writeDelta(w, schema, state, dirty, tick) {
2221
2503
  w.u32(tick);
@@ -2258,6 +2540,7 @@ function writeDelta(w, schema, state, dirty, tick) {
2258
2540
  f.type,
2259
2541
  value[f.name],
2260
2542
  op.mask.nested.get(i),
2543
+ op.mask.ranges?.get(i),
2261
2544
  `${c.name}[${op.id}].${f.name}`
2262
2545
  );
2263
2546
  }
@@ -2278,6 +2561,7 @@ function readBit(mask, i) {
2278
2561
  return ((mask[i >> 3] ?? 0) & 1 << (i & 7)) !== 0;
2279
2562
  }
2280
2563
  function readDirtyField(r, desc) {
2564
+ if (desc.kind === "bytes") return readDirtyBytes(r, desc);
2281
2565
  if (desc.kind !== "struct") return { value: readValue(r, desc) };
2282
2566
  if (desc.opt && r.u8() === 0) return { value: void 0 };
2283
2567
  const names = structNames(desc);
@@ -2292,41 +2576,78 @@ function readDirtyField(r, desc) {
2292
2576
  out[name] = got.value;
2293
2577
  mask.fields.add(i);
2294
2578
  if (got.nested) mask.nested.set(i, got.nested);
2579
+ if (got.ranges) (mask.ranges ??= /* @__PURE__ */ new Map()).set(i, got.ranges);
2295
2580
  }
2296
2581
  return { value: out, nested: mask };
2297
2582
  }
2298
- function decodeDelta(schema, bytes) {
2299
- return decodeDeltaFrom(schema, new ByteReader(bytes));
2583
+ function readDirtyBytes(r, desc) {
2584
+ if (desc.opt && r.u8() === 0) return { value: void 0 };
2585
+ const tag = r.u8();
2586
+ if (tag === BYTES_WHOLE) return { value: copyBytes(r.bytes(desc.n)) };
2587
+ if (tag !== BYTES_PATCH) throw new Error(`unknown bytes tag ${tag}`);
2588
+ const count = r.varint();
2589
+ if (count < 1 || count > MAX_BYTE_RANGES) {
2590
+ throw new Error(`bytes patch has ${count} spans (1..${MAX_BYTE_RANGES} allowed)`);
2591
+ }
2592
+ const spans = [];
2593
+ const ranges = [];
2594
+ let floor = 0;
2595
+ for (let i = 0; i < count; i++) {
2596
+ const offset = r.varint();
2597
+ const length = r.varint();
2598
+ if (length < 1) throw new Error("bytes patch span has length 0");
2599
+ if (offset < floor || offset + length > desc.n) {
2600
+ throw new Error(
2601
+ `bytes patch span [${offset}, ${offset + length}) is out of order or outside bytes(${desc.n})`
2602
+ );
2603
+ }
2604
+ spans.push({ offset, bytes: copyBytes(r.bytes(length)) });
2605
+ ranges.push({ start: offset, end: offset + length });
2606
+ floor = offset + length;
2607
+ }
2608
+ const patch = { spans };
2609
+ return { value: patch, ranges };
2610
+ }
2611
+ function decodeDelta(schema, bytes2, options) {
2612
+ return decodeDeltaFrom(schema, new ByteReader(bytes2), options);
2300
2613
  }
2301
- function decodeDeltaFrom(schema, r) {
2614
+ function decodeDeltaFrom(schema, r, options = {}) {
2302
2615
  const tick = r.u32();
2303
2616
  const hash8 = r.bytes(8).slice();
2304
2617
  const collections = [];
2305
2618
  const count = r.varint();
2619
+ const known = schema.collections.length;
2620
+ if (count > known) throw new Error(`delta names ${count} collections; the schema has ${known}`);
2621
+ let total = 0;
2306
2622
  for (let ci = 0; ci < count; ci++) {
2307
2623
  const index = r.varint();
2308
2624
  const c = schema.collections[index];
2309
2625
  if (!c) throw new Error(`delta references unknown collection index ${index}`);
2310
2626
  const opCount = r.varint();
2627
+ if (opCount > r.remaining)
2628
+ throw new Error(`delta claims ${opCount} ops in ${c.name} with ${r.remaining} bytes left`);
2629
+ total += opCount;
2630
+ if (options.maxOps !== void 0 && total > options.maxOps)
2631
+ throw new Error(`delta carries ${total} ops; the limit is ${options.maxOps}`);
2311
2632
  const ops = [];
2312
2633
  for (let oi = 0; oi < opCount; oi++) {
2313
2634
  const op = r.u8();
2314
2635
  if (op === OP_REMOVE) {
2315
- ops.push({ op: "remove", id: r.str() });
2636
+ ops.push({ op: "remove", id: r.str(ID_MAX_BYTES) });
2316
2637
  continue;
2317
2638
  }
2318
2639
  if (op === OP_ADD) {
2319
- const id2 = r.str();
2320
- const owner2 = r.str();
2640
+ const id2 = r.str(ID_MAX_BYTES);
2641
+ const owner2 = r.str(ID_MAX_BYTES);
2321
2642
  ops.push({ op: "add", id: id2, owner: owner2, value: readRecord(r, c.fields) });
2322
2643
  continue;
2323
2644
  }
2324
2645
  if (op !== OP_UPDATE) throw new Error(`unknown delta op byte ${op} in ${c.name}`);
2325
- const id = r.str();
2646
+ const id = r.str(ID_MAX_BYTES);
2326
2647
  const n = c.fields.length;
2327
2648
  const maskBytes = r.bytes(n + 1 + 7 >> 3);
2328
2649
  const ownerDirty = readBit(maskBytes, n);
2329
- const owner = ownerDirty ? r.str() : void 0;
2650
+ const owner = ownerDirty ? r.str(ID_MAX_BYTES) : void 0;
2330
2651
  const mask = createFieldMask();
2331
2652
  const patch = {};
2332
2653
  for (let i = 0; i < n; i++) {
@@ -2336,6 +2657,7 @@ function decodeDeltaFrom(schema, r) {
2336
2657
  patch[f.name] = got.value;
2337
2658
  mask.fields.add(i);
2338
2659
  if (got.nested) mask.nested.set(i, got.nested);
2660
+ if (got.ranges) (mask.ranges ??= /* @__PURE__ */ new Map()).set(i, got.ranges);
2339
2661
  }
2340
2662
  ops.push(
2341
2663
  owner === void 0 ? { op: "update", id, mask, patch } : { op: "update", id, owner, mask, patch }
@@ -2366,9 +2688,23 @@ function applyPatch(fieldTypes, target, patch, mask) {
2366
2688
  continue;
2367
2689
  }
2368
2690
  }
2691
+ if (mask.ranges?.has(i) && f.type.kind === "bytes" && isBytesPatch(value)) {
2692
+ const current = target[f.name];
2693
+ if (current instanceof Uint8Array && current.length === f.type.n) {
2694
+ for (const span of value.spans) current.set(span.bytes, span.offset);
2695
+ continue;
2696
+ }
2697
+ const fresh = new Uint8Array(f.type.n);
2698
+ for (const span of value.spans) fresh.set(span.bytes, span.offset);
2699
+ target[f.name] = fresh;
2700
+ continue;
2701
+ }
2369
2702
  target[f.name] = value;
2370
2703
  }
2371
2704
  }
2705
+ function isBytesPatch(v) {
2706
+ return typeof v === "object" && v !== null && Array.isArray(v.spans);
2707
+ }
2372
2708
  function applyDelta(schema, state, delta) {
2373
2709
  for (const dc of delta.collections) {
2374
2710
  const c = schema.collection(dc.name);
@@ -2397,6 +2733,9 @@ function applyDelta(schema, state, delta) {
2397
2733
  }
2398
2734
  function valueEquals(a, b) {
2399
2735
  if (Object.is(a, b)) return true;
2736
+ if (a instanceof Uint8Array || b instanceof Uint8Array) {
2737
+ return a instanceof Uint8Array && b instanceof Uint8Array && bytesEqual(a, b);
2738
+ }
2400
2739
  if (Array.isArray(a) || Array.isArray(b)) {
2401
2740
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
2402
2741
  for (let i = 0; i < a.length; i++) if (!valueEquals(a[i], b[i])) return false;
@@ -2422,6 +2761,14 @@ function diffField(desc, a, b, mask, idx) {
2422
2761
  mask.nested.set(idx, nested);
2423
2762
  return;
2424
2763
  }
2764
+ if (desc.kind === "bytes" && a instanceof Uint8Array && b instanceof Uint8Array && a.length === desc.n && b.length === desc.n) {
2765
+ const found = diffByteRanges(a, b);
2766
+ if (found.length === 0) return;
2767
+ mask.fields.add(idx);
2768
+ const settled = settleRanges(found, desc.n);
2769
+ if (settled) (mask.ranges ??= /* @__PURE__ */ new Map()).set(idx, settled);
2770
+ return;
2771
+ }
2425
2772
  if (!valueEquals(a, b)) mask.fields.add(idx);
2426
2773
  }
2427
2774
  function diffRecord(c, a, b, mask) {
@@ -2491,6 +2838,9 @@ function skipValue(r, desc) {
2491
2838
  case "ref":
2492
2839
  skip(r, r.varint());
2493
2840
  return;
2841
+ case "bytes":
2842
+ skip(r, desc.n);
2843
+ return;
2494
2844
  case "list": {
2495
2845
  const n = r.varint();
2496
2846
  if (n > desc.max) throw new Error(`list exceeds max length ${desc.max} (${n})`);
@@ -2507,6 +2857,33 @@ function readBit2(mask, i) {
2507
2857
  return ((mask[i >> 3] ?? 0) & 1 << (i & 7)) !== 0;
2508
2858
  }
2509
2859
  function skipDirtyField(r, desc) {
2860
+ if (desc.kind === "bytes") {
2861
+ if (desc.opt && r.u8() === 0) return;
2862
+ const tag = r.u8();
2863
+ if (tag === 0) {
2864
+ skip(r, desc.n);
2865
+ return;
2866
+ }
2867
+ if (tag !== 1) throw new Error(`unknown bytes tag ${tag}`);
2868
+ const count = r.varint();
2869
+ if (count < 1 || count > MAX_BYTE_RANGES) {
2870
+ throw new Error(`bytes patch has ${count} spans (1..${MAX_BYTE_RANGES} allowed)`);
2871
+ }
2872
+ let floor = 0;
2873
+ for (let i = 0; i < count; i++) {
2874
+ const offset = r.varint();
2875
+ const length = r.varint();
2876
+ if (length < 1) throw new Error("bytes patch span has length 0");
2877
+ if (offset < floor || offset + length > desc.n) {
2878
+ throw new Error(
2879
+ `bytes patch span [${offset}, ${offset + length}) is out of order or outside bytes(${desc.n})`
2880
+ );
2881
+ }
2882
+ skip(r, length);
2883
+ floor = offset + length;
2884
+ }
2885
+ return;
2886
+ }
2510
2887
  if (desc.kind !== "struct") {
2511
2888
  skipValue(r, desc);
2512
2889
  return;
@@ -2519,11 +2896,11 @@ function skipDirtyField(r, desc) {
2519
2896
  skipDirtyField(r, desc.fields[names[i]]);
2520
2897
  }
2521
2898
  }
2522
- function readerFor(bytes) {
2523
- return bytes instanceof ByteReader ? bytes : new ByteReader(bytes);
2899
+ function readerFor(bytes2) {
2900
+ return bytes2 instanceof ByteReader ? bytes2 : new ByteReader(bytes2);
2524
2901
  }
2525
- function walkDelta(schema, bytes, events) {
2526
- const r = readerFor(bytes);
2902
+ function walkDelta(schema, bytes2, events) {
2903
+ const r = readerFor(bytes2);
2527
2904
  const from = r.pos;
2528
2905
  let at = r.pos;
2529
2906
  skip(r, 12);
@@ -2570,8 +2947,8 @@ function walkDelta(schema, bytes, events) {
2570
2947
  }
2571
2948
  return r.pos - from;
2572
2949
  }
2573
- function walkSnapshot(schema, bytes, events) {
2574
- const r = readerFor(bytes);
2950
+ function walkSnapshot(schema, bytes2, events) {
2951
+ const r = readerFor(bytes2);
2575
2952
  const from = r.pos;
2576
2953
  skip(r, 12);
2577
2954
  events.header(12);
@@ -2631,8 +3008,9 @@ function mark(root, path) {
2631
3008
  }
2632
3009
  function assertLive(root) {
2633
3010
  if (!root.live) {
3011
+ const id = JSON.stringify(root.id);
2634
3012
  throw new Error(
2635
- `${root.collection}: instance ${JSON.stringify(root.id)} was removed; its proxy is stale`
3013
+ root.reason === "replaced" ? `${root.collection}: instance ${id} was replaced (re-added) since this handle was taken; re-read it with .get(${id})` : `${root.collection}: instance ${id} was removed; its proxy is stale`
2636
3014
  );
2637
3015
  }
2638
3016
  }
@@ -2653,6 +3031,7 @@ function objHandler(node) {
2653
3031
  return structProxy(node, f, v);
2654
3032
  }
2655
3033
  if (f.type.kind === "list") return listProxy(node, f, v);
3034
+ if (f.type.kind === "bytes") return bytesProxy(node, f, v);
2656
3035
  return v;
2657
3036
  },
2658
3037
  set(target, prop, value) {
@@ -2665,6 +3044,16 @@ function objHandler(node) {
2665
3044
  const at = `${node.label}.${f.name}`;
2666
3045
  const next = value === void 0 && f.type.opt ? void 0 : normalizeValue(f.type, value, at);
2667
3046
  if (valueEquals(target[f.name], next)) return true;
3047
+ const current = target[f.name];
3048
+ if (f.type.kind === "bytes" && !node.whole && current instanceof Uint8Array && next instanceof Uint8Array) {
3049
+ const path = childPath(node.path, node.whole, f.index);
3050
+ const root = node.root;
3051
+ for (const r of diffByteRanges(current, next)) {
3052
+ markBytes(root.ctx.dirty, root.collection, root.id, path, r.start, r.end, f.type.n);
3053
+ }
3054
+ current.set(next);
3055
+ return true;
3056
+ }
2668
3057
  target[f.name] = next;
2669
3058
  mark(node.root, childPath(node.path, node.whole, f.index));
2670
3059
  return true;
@@ -2832,6 +3221,177 @@ function listElemProxy(parent, prop, value) {
2832
3221
  label: `${parent.label}[${prop}]`
2833
3222
  });
2834
3223
  }
3224
+ var BYTES_MUTATORS = /* @__PURE__ */ new Set(["set", "fill", "copyWithin", "sort", "reverse"]);
3225
+ function isNumericProp(prop) {
3226
+ return prop === "-0" || String(Number(prop)) === prop;
3227
+ }
3228
+ function relIndex(x, len, dflt) {
3229
+ if (x === void 0) return dflt;
3230
+ const v = Math.trunc(Number(x)) || 0;
3231
+ return v < 0 ? Math.max(len + v, 0) : Math.min(v, len);
3232
+ }
3233
+ function checkByte(label, i, v) {
3234
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > 255) {
3235
+ throw new Error(`${label}[${i}]: must be an integer 0..255 (got ${String(v)})`);
3236
+ }
3237
+ }
3238
+ function reportDiff(t, before, start, hooks) {
3239
+ let first = -1;
3240
+ let last = -1;
3241
+ for (let i = 0; i < before.length; i++) {
3242
+ if (before[i] !== t[start + i]) {
3243
+ if (first < 0) first = i;
3244
+ last = i;
3245
+ }
3246
+ }
3247
+ if (first >= 0) hooks.changed(start + first, start + last + 1);
3248
+ }
3249
+ function bytesView(target, label, hooks) {
3250
+ const n = target.length;
3251
+ const bound = /* @__PURE__ */ new Map();
3252
+ const mutator = (name) => {
3253
+ const t = target;
3254
+ switch (name) {
3255
+ case "set":
3256
+ return (src, offset) => {
3257
+ if (!hooks.begin("set")) return void 0;
3258
+ const source = src;
3259
+ const len = Number(source?.length ?? 0) || 0;
3260
+ const off = Math.trunc(Number(offset ?? 0)) || 0;
3261
+ if (off < 0 || off + len > n) {
3262
+ throw new Error(
3263
+ `${label}.set: a source of length ${len} at offset ${off} overflows bytes(${n})`
3264
+ );
3265
+ }
3266
+ if (!(src instanceof Uint8Array) && !(src instanceof Uint8ClampedArray)) {
3267
+ for (let i = 0; i < len; i++) checkByte(label, off + i, source[i]);
3268
+ }
3269
+ const before = t.slice(off, off + len);
3270
+ try {
3271
+ t.set(src, off);
3272
+ } catch (e) {
3273
+ if (e instanceof RangeError) {
3274
+ throw new Error(`${label}.set: ${e.message} (bytes(${n}))`);
3275
+ }
3276
+ throw e;
3277
+ }
3278
+ reportDiff(t, before, off, hooks);
3279
+ return void 0;
3280
+ };
3281
+ case "fill":
3282
+ return (v, start, end) => {
3283
+ if (!hooks.begin("fill")) return proxy;
3284
+ checkByte(label, "fill", v);
3285
+ const s = relIndex(start, n, 0);
3286
+ const e = relIndex(end, n, n);
3287
+ if (e > s) {
3288
+ const before = t.slice(s, e);
3289
+ t.fill(v, s, e);
3290
+ reportDiff(t, before, s, hooks);
3291
+ }
3292
+ return proxy;
3293
+ };
3294
+ case "copyWithin":
3295
+ return (to, start, end) => {
3296
+ if (!hooks.begin("copyWithin")) return proxy;
3297
+ const dst = relIndex(to, n, 0);
3298
+ const from = relIndex(start, n, 0);
3299
+ const fin = relIndex(end, n, n);
3300
+ const count = Math.min(fin - from, n - dst);
3301
+ if (count > 0) {
3302
+ const before = t.slice(dst, dst + count);
3303
+ t.copyWithin(dst, from, fin);
3304
+ reportDiff(t, before, dst, hooks);
3305
+ }
3306
+ return proxy;
3307
+ };
3308
+ default:
3309
+ return (...args) => {
3310
+ if (!hooks.begin(name)) return proxy;
3311
+ const before = t.slice();
3312
+ Uint8Array.prototype[name].apply(t, args);
3313
+ for (let i = 0; i < n; i++) {
3314
+ if (before[i] !== t[i]) {
3315
+ hooks.changed(0, n);
3316
+ break;
3317
+ }
3318
+ }
3319
+ return proxy;
3320
+ };
3321
+ }
3322
+ };
3323
+ const selfValue = () => proxy;
3324
+ const proxy = new Proxy(target, {
3325
+ get(t, prop) {
3326
+ if (typeof prop === "string") {
3327
+ if (isIndexProp(prop)) return t[prop];
3328
+ if (BYTES_MUTATORS.has(prop)) {
3329
+ let fn = bound.get(prop);
3330
+ if (!fn) {
3331
+ fn = mutator(prop);
3332
+ bound.set(prop, fn);
3333
+ }
3334
+ return fn;
3335
+ }
3336
+ }
3337
+ if (prop === "valueOf") return selfValue;
3338
+ const v = Reflect.get(t, prop, t);
3339
+ if (typeof v === "function" && prop !== "constructor") {
3340
+ let fn = bound.get(prop);
3341
+ if (!fn) {
3342
+ fn = v.bind(t);
3343
+ bound.set(prop, fn);
3344
+ }
3345
+ return fn;
3346
+ }
3347
+ return v;
3348
+ },
3349
+ set(t, prop, value) {
3350
+ if (typeof prop === "string" && isNumericProp(prop)) {
3351
+ if (!hooks.begin(prop)) return true;
3352
+ const i = Number(prop);
3353
+ if (!Number.isInteger(i) || i < 0 || i >= n || prop === "-0") {
3354
+ throw new Error(`${label}[${prop}]: index out of range for bytes(${n})`);
3355
+ }
3356
+ checkByte(label, i, value);
3357
+ if (t[i] !== value) {
3358
+ t[i] = value;
3359
+ hooks.changed(i, i + 1);
3360
+ }
3361
+ return true;
3362
+ }
3363
+ if (!hooks.begin(String(prop))) return true;
3364
+ throw new Error(`${label}: cannot set ${String(prop)} on a bytes(${n}) field`);
3365
+ },
3366
+ deleteProperty(_t, prop) {
3367
+ if (!hooks.begin(String(prop))) return true;
3368
+ throw new Error(`${label}: cannot delete ${String(prop)} from a bytes(${n}) field`);
3369
+ },
3370
+ defineProperty(_t, prop) {
3371
+ if (!hooks.begin(String(prop))) return true;
3372
+ throw new Error(`${label}: cannot defineProperty ${String(prop)} on a bytes(${n}) field`);
3373
+ }
3374
+ });
3375
+ return proxy;
3376
+ }
3377
+ function bytesProxy(parent, f, value) {
3378
+ const root = parent.root;
3379
+ const cached = root.ctx.proxies.get(value);
3380
+ if (cached) return cached;
3381
+ const path = childPath(parent.path, parent.whole, f.index);
3382
+ const n = f.type.n;
3383
+ const p = bytesView(value, `${parent.label}.${f.name}`, {
3384
+ begin() {
3385
+ assertLive(root);
3386
+ return true;
3387
+ },
3388
+ // `whole` (a bytes field inside a list element) cannot happen today — `list()` refuses bytes
3389
+ // items — but if it ever can, the list field is what has to be marked.
3390
+ changed: parent.whole ? () => mark(root, path) : (start, end) => markBytes(root.ctx.dirty, root.collection, root.id, path, start, end, n)
3391
+ });
3392
+ root.ctx.proxies.set(value, p);
3393
+ return p;
3394
+ }
2835
3395
  var TrackedCollection = class {
2836
3396
  constructor(ctx, desc, inner) {
2837
3397
  this.ctx = ctx;
@@ -2842,10 +3402,11 @@ var TrackedCollection = class {
2842
3402
  desc;
2843
3403
  inner;
2844
3404
  roots = /* @__PURE__ */ new Map();
2845
- invalidate(id) {
3405
+ invalidate(id, reason) {
2846
3406
  const root = this.roots.get(id);
2847
3407
  if (root) {
2848
3408
  root.live = false;
3409
+ root.reason = reason;
2849
3410
  this.roots.delete(id);
2850
3411
  }
2851
3412
  }
@@ -2854,7 +3415,10 @@ var TrackedCollection = class {
2854
3415
  if (cached) return cached;
2855
3416
  let root = this.roots.get(id);
2856
3417
  if (!root || root.target !== value) {
2857
- if (root) root.live = false;
3418
+ if (root) {
3419
+ root.live = false;
3420
+ root.reason = "replaced";
3421
+ }
2858
3422
  root = { ctx: this.ctx, collection: this.desc.name, id, target: value, live: true };
2859
3423
  this.roots.set(id, root);
2860
3424
  }
@@ -2868,7 +3432,7 @@ var TrackedCollection = class {
2868
3432
  });
2869
3433
  }
2870
3434
  add(id, values, options) {
2871
- this.invalidate(id);
3435
+ this.invalidate(id, this.inner.has(id) ? "replaced" : "removed");
2872
3436
  const value = this.inner.add(id, values, options);
2873
3437
  markAdd(this.ctx.dirty, this.desc.name, id);
2874
3438
  return this.proxyFor(id, value);
@@ -2876,7 +3440,7 @@ var TrackedCollection = class {
2876
3440
  remove(id) {
2877
3441
  const had = this.inner.remove(id);
2878
3442
  if (had) {
2879
- this.invalidate(id);
3443
+ this.invalidate(id, "removed");
2880
3444
  markRemove(this.ctx.dirty, this.desc.name, id);
2881
3445
  }
2882
3446
  return had;
@@ -3001,6 +3565,18 @@ function frozenProxy(value, ownershipHint) {
3001
3565
  function freeze(target, hint, prefix, warned, cache) {
3002
3566
  const cached = cache.get(target);
3003
3567
  if (cached) return cached;
3568
+ if (target instanceof Uint8Array) {
3569
+ const view = bytesView(target, prefix, {
3570
+ begin(member) {
3571
+ warnFrozen(hint, prefix, member, warned);
3572
+ return false;
3573
+ },
3574
+ changed() {
3575
+ }
3576
+ });
3577
+ cache.set(target, view);
3578
+ return view;
3579
+ }
3004
3580
  const p = new Proxy(target, {
3005
3581
  get(t, prop, receiver) {
3006
3582
  const v = Reflect.get(t, prop, receiver);
@@ -3035,17 +3611,34 @@ function warnFrozen(hint, prefix, prop, warned) {
3035
3611
  // src/diff.ts
3036
3612
  function diffSchemas(oldSchema, newSchema) {
3037
3613
  const changes = [];
3038
- diffCollections(
3039
- oldSchema.collections,
3040
- newSchema.collections,
3041
- changes
3614
+ const section = (scope, diff) => {
3615
+ const out = [];
3616
+ diff(out);
3617
+ for (const c of out) changes.push({ ...c, scope });
3618
+ };
3619
+ section(
3620
+ "collection",
3621
+ (out) => diffCollections(
3622
+ oldSchema.collections,
3623
+ newSchema.collections,
3624
+ out
3625
+ )
3042
3626
  );
3043
- diffRpcs(oldSchema.rpcs, newSchema.rpcs, changes);
3044
- diffRoles(oldSchema.roles, newSchema.roles, changes);
3045
- diffMessages(
3046
- oldSchema.messages ?? [],
3047
- newSchema.messages ?? [],
3048
- changes
3627
+ section(
3628
+ "rpc",
3629
+ (out) => diffRpcs(oldSchema.rpcs, newSchema.rpcs, out)
3630
+ );
3631
+ section(
3632
+ "roles",
3633
+ (out) => diffRoles(oldSchema.roles, newSchema.roles, out)
3634
+ );
3635
+ section(
3636
+ "message",
3637
+ (out) => diffMessages(
3638
+ oldSchema.messages ?? [],
3639
+ newSchema.messages ?? [],
3640
+ out
3641
+ )
3049
3642
  );
3050
3643
  changes.sort(
3051
3644
  (a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
@@ -3054,9 +3647,7 @@ function diffSchemas(oldSchema, newSchema) {
3054
3647
  }
3055
3648
  function breakingIsWireOnly(changes) {
3056
3649
  const breaking = changes.filter((c) => c.kind === "breaking");
3057
- return breaking.length > 0 && breaking.every(
3058
- (c) => c.path === "rpc" || c.path.startsWith("rpc.") || c.path.startsWith("message.")
3059
- );
3650
+ return breaking.length > 0 && breaking.every((c) => c.scope === "rpc" || c.scope === "message");
3060
3651
  }
3061
3652
  function diffCollections(oldCollections, newCollections, changes) {
3062
3653
  const oldByName = new Map(oldCollections.map((c) => [c.name, c]));
@@ -3250,6 +3841,24 @@ function diffRoles(oldRoles, newRoles, changes) {
3250
3841
  }
3251
3842
  }
3252
3843
  }
3844
+ function defaultHint(t) {
3845
+ switch (t.kind) {
3846
+ case "bool":
3847
+ return "add `.default(false)`, or make it .opt";
3848
+ case "str":
3849
+ return "add `.default('')`, or make it .opt";
3850
+ case "enum":
3851
+ return `add \`.default(${JSON.stringify(t.values[0])})\`, or make it .opt`;
3852
+ case "list":
3853
+ return "add `.default([])`, or make it .opt";
3854
+ case "struct":
3855
+ return "make it .opt, or add `.default({ ... })` with every field filled";
3856
+ case "ref":
3857
+ return "make it .opt (a ref names an instance; there is no default worth inventing)";
3858
+ default:
3859
+ return `add \`.default(0)\` to the ${t.kind}, or make it .opt`;
3860
+ }
3861
+ }
3253
3862
  function diffFields(oldFields, newFields, basePath, changes) {
3254
3863
  const oldByName = new Map(oldFields.map((f) => [f.name, f]));
3255
3864
  const newByName = new Map(newFields.map((f) => [f.name, f]));
@@ -3270,7 +3879,7 @@ function diffFields(oldFields, newFields, basePath, changes) {
3270
3879
  kind: additive ? "additive" : "breaking",
3271
3880
  code: "field.added",
3272
3881
  path: `${basePath}.${name}`,
3273
- message: additive ? `${basePath}.${name}: new field added (has a default/opt, safe for existing snapshots)` : `${basePath}.${name}: new required field without a default (breaking) \u2014 add .default(...) or make it .opt`
3882
+ message: additive ? `${basePath}.${name}: new field added (has a default/opt, safe for existing snapshots)` : `${basePath}.${name}: new required field without a default (breaking) \u2014 ${defaultHint(f.type)}`
3274
3883
  });
3275
3884
  }
3276
3885
  if (removedNames.length === 1 && addedNames.length === 1) {
@@ -3347,6 +3956,18 @@ function diffType(oldT, newT, path, changes) {
3347
3956
  case "enum":
3348
3957
  diffEnum(oldT.values, newT.values, path, changes);
3349
3958
  break;
3959
+ case "bytes": {
3960
+ const newN = newT.n;
3961
+ if (oldT.n !== newN) {
3962
+ changes.push({
3963
+ kind: "breaking",
3964
+ code: "field.bytes_length_changed",
3965
+ path,
3966
+ message: `${path}: bytes length changed from ${oldT.n} to ${newN} \u2014 the length is implied by the schema, so every stored snapshot would be misread (breaking)`
3967
+ });
3968
+ }
3969
+ break;
3970
+ }
3350
3971
  case "list": {
3351
3972
  const newList = newT;
3352
3973
  diffMax(oldT.max, newList.max, path, changes);
@@ -3413,7 +4034,7 @@ function diffDefault(oldT, newT, path, changes) {
3413
4034
  path,
3414
4035
  message: `${path}: .default(...) removed; existing snapshots already have values, so this is safe`
3415
4036
  });
3416
- } else if (oldT.hasDefault && newT.hasDefault && stableStringify(oldT.default) !== stableStringify(newT.default)) {
4037
+ } else if (oldT.hasDefault && newT.hasDefault && !defaultsEqual(oldT, newT)) {
3417
4038
  changes.push({
3418
4039
  kind: "additive",
3419
4040
  code: "field.default_changed",
@@ -3422,6 +4043,15 @@ function diffDefault(oldT, newT, path, changes) {
3422
4043
  });
3423
4044
  }
3424
4045
  }
4046
+ function defaultsEqual(oldT, newT) {
4047
+ if (oldT.default instanceof Uint8Array && newT.default instanceof Uint8Array) {
4048
+ return bytesEqual(oldT.default, newT.default);
4049
+ }
4050
+ if (containsBytes(oldT) || containsBytes(newT)) {
4051
+ return stableStringify(jsonDefault(oldT, oldT.default)) === stableStringify(jsonDefault(newT, newT.default));
4052
+ }
4053
+ return stableStringify(oldT.default) === stableStringify(newT.default);
4054
+ }
3425
4055
  function structFieldDescs(fields) {
3426
4056
  return Object.entries(fields).map(([name, type], index) => ({ name, index, type }));
3427
4057
  }
@@ -3448,6 +4078,9 @@ function typeSignature(d) {
3448
4078
  case "ref":
3449
4079
  o.entity = d.entity;
3450
4080
  break;
4081
+ case "bytes":
4082
+ o.n = d.n;
4083
+ break;
3451
4084
  case "list":
3452
4085
  o.max = d.max;
3453
4086
  o.item = typeSignature(d.item);
@@ -3461,11 +4094,13 @@ function typeSignature(d) {
3461
4094
  return stableStringify(o);
3462
4095
  }
3463
4096
  export {
4097
+ BYTES_MAX,
3464
4098
  ByteReader,
3465
4099
  ByteWriter,
3466
4100
  CANONICAL_VERSION,
3467
4101
  EntityCollection,
3468
4102
  ID_MAX_BYTES,
4103
+ MAX_BYTE_RANGES,
3469
4104
  MAX_MESSAGES,
3470
4105
  MAX_USER_RPCS,
3471
4106
  PERSIST_BLOB_BYTES,
@@ -3478,9 +4113,12 @@ export {
3478
4113
  angleFrom2d,
3479
4114
  applyChannel2d,
3480
4115
  applyDelta,
4116
+ base64ToBytes,
3481
4117
  bool,
3482
4118
  breakingIsWireOnly,
4119
+ bytes,
3483
4120
  bytesEqual,
4121
+ bytesToBase64,
3484
4122
  canonicalOwnership,
3485
4123
  canonicalPhysics,
3486
4124
  canonicalType,
@@ -3491,6 +4129,8 @@ export {
3491
4129
  compileOwnership,
3492
4130
  compilePhysics,
3493
4131
  computeDirty,
4132
+ containsBytes,
4133
+ copyBytes,
3494
4134
  createDirtySet,
3495
4135
  createFieldMask,
3496
4136
  createState,
@@ -3502,6 +4142,7 @@ export {
3502
4142
  defaultValue,
3503
4143
  defineSchema,
3504
4144
  describeType,
4145
+ diffByteRanges,
3505
4146
  diffSchemas,
3506
4147
  encodeDelta,
3507
4148
  encodeFields,
@@ -3518,6 +4159,8 @@ export {
3518
4159
  isWhole,
3519
4160
  list,
3520
4161
  markAdd,
4162
+ markBytes,
4163
+ markBytesPath,
3521
4164
  markField,
3522
4165
  markOwner,
3523
4166
  markPath,
@@ -3530,11 +4173,13 @@ export {
3530
4173
  ownershipFromCanonical,
3531
4174
  parseHold,
3532
4175
  physicsFromCanonical,
4176
+ rangesPatchBytes,
3533
4177
  readValue,
3534
4178
  reconcileCollection,
3535
4179
  ref,
3536
4180
  schemaFromCanonical,
3537
4181
  server,
4182
+ settleRanges,
3538
4183
  sha256,
3539
4184
  sha256Hex,
3540
4185
  singleton,
@@ -3546,6 +4191,7 @@ export {
3546
4191
  u16,
3547
4192
  u32,
3548
4193
  u8,
4194
+ unionRanges,
3549
4195
  utf8Length,
3550
4196
  validateForDeploy,
3551
4197
  validateValue,