@irtio/schema 0.1.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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +704 -0
- package/dist/index.js +2524 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2524 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var TypeImpl = class _TypeImpl {
|
|
3
|
+
constructor(desc) {
|
|
4
|
+
this.desc = desc;
|
|
5
|
+
}
|
|
6
|
+
desc;
|
|
7
|
+
get opt() {
|
|
8
|
+
return new _TypeImpl({ ...this.desc, opt: true });
|
|
9
|
+
}
|
|
10
|
+
default(value) {
|
|
11
|
+
if (value === void 0) {
|
|
12
|
+
throw new Error(".default(undefined) is not a default \u2014 use .opt on its own");
|
|
13
|
+
}
|
|
14
|
+
const err = validateValue(this.desc, value);
|
|
15
|
+
if (err) throw new Error(`invalid default for ${describeType(this.desc)}: ${err}`);
|
|
16
|
+
return new _TypeImpl({ ...this.desc, hasDefault: true, default: value });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function make(desc) {
|
|
20
|
+
return new TypeImpl({ ...desc, opt: false, hasDefault: false });
|
|
21
|
+
}
|
|
22
|
+
var bool = make({ kind: "bool" });
|
|
23
|
+
var u8 = make({ kind: "u8" });
|
|
24
|
+
var u16 = make({ kind: "u16" });
|
|
25
|
+
var u32 = make({ kind: "u32" });
|
|
26
|
+
var i32 = make({ kind: "i32" });
|
|
27
|
+
var f32 = make({ kind: "f32" });
|
|
28
|
+
var f64 = make({ kind: "f64" });
|
|
29
|
+
function str(max) {
|
|
30
|
+
assertMax(max, "str");
|
|
31
|
+
return make({ kind: "str", max });
|
|
32
|
+
}
|
|
33
|
+
function enumOf(...values) {
|
|
34
|
+
if (values.length === 0) throw new Error("enumOf needs at least one value");
|
|
35
|
+
if (values.length > 256) throw new Error("enumOf supports at most 256 values");
|
|
36
|
+
if (new Set(values).size !== values.length) throw new Error("enumOf values must be unique");
|
|
37
|
+
return make({ kind: "enum", values: [...values] });
|
|
38
|
+
}
|
|
39
|
+
function ref(entity2) {
|
|
40
|
+
return make({ kind: "ref", entity: entity2 });
|
|
41
|
+
}
|
|
42
|
+
function list(item, max) {
|
|
43
|
+
assertMax(max, "list");
|
|
44
|
+
return make({ kind: "list", item: item.desc, max });
|
|
45
|
+
}
|
|
46
|
+
function struct(fields) {
|
|
47
|
+
const descs = {};
|
|
48
|
+
for (const [k, t] of Object.entries(fields)) {
|
|
49
|
+
assertFieldName(k);
|
|
50
|
+
descs[k] = t.desc;
|
|
51
|
+
}
|
|
52
|
+
return make({ kind: "struct", fields: descs });
|
|
53
|
+
}
|
|
54
|
+
function assertMax(max, what) {
|
|
55
|
+
if (!Number.isInteger(max) || max < 0 || max > 4294967295) {
|
|
56
|
+
throw new Error(`${what}(max): max must be a non-negative integer, got ${String(max)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function assertFieldName(name) {
|
|
60
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
|
|
61
|
+
throw new Error(`invalid field name ${JSON.stringify(name)}: must be an identifier`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function describeType(d) {
|
|
65
|
+
let s;
|
|
66
|
+
switch (d.kind) {
|
|
67
|
+
case "str":
|
|
68
|
+
s = `str(${d.max})`;
|
|
69
|
+
break;
|
|
70
|
+
case "enum":
|
|
71
|
+
s = `enumOf(${d.values.map((v) => JSON.stringify(v)).join(", ")})`;
|
|
72
|
+
break;
|
|
73
|
+
case "ref":
|
|
74
|
+
s = `ref(${JSON.stringify(d.entity)})`;
|
|
75
|
+
break;
|
|
76
|
+
case "list":
|
|
77
|
+
s = `list(${describeType(d.item)}, ${d.max})`;
|
|
78
|
+
break;
|
|
79
|
+
case "struct":
|
|
80
|
+
s = `struct({ ${Object.entries(d.fields).map(([k, v]) => `${k}: ${describeType(v)}`).join(", ")} })`;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
s = d.kind;
|
|
84
|
+
}
|
|
85
|
+
return d.opt ? `${s}.opt` : s;
|
|
86
|
+
}
|
|
87
|
+
var utf8 = new TextEncoder();
|
|
88
|
+
function utf8Length(s) {
|
|
89
|
+
return utf8.encode(s).length;
|
|
90
|
+
}
|
|
91
|
+
var INT_RANGE = {
|
|
92
|
+
u8: [0, 255],
|
|
93
|
+
u16: [0, 65535],
|
|
94
|
+
u32: [0, 4294967295],
|
|
95
|
+
i32: [-2147483648, 2147483647]
|
|
96
|
+
};
|
|
97
|
+
var ID_MAX_BYTES = 32;
|
|
98
|
+
function validateValue(d, v, path = "") {
|
|
99
|
+
if (v === void 0) return d.opt ? null : `${path || "value"} is required`;
|
|
100
|
+
const at = path || "value";
|
|
101
|
+
switch (d.kind) {
|
|
102
|
+
case "bool":
|
|
103
|
+
return typeof v === "boolean" ? null : `${at} must be a boolean`;
|
|
104
|
+
case "u8":
|
|
105
|
+
case "u16":
|
|
106
|
+
case "u32":
|
|
107
|
+
case "i32": {
|
|
108
|
+
if (typeof v !== "number" || !Number.isInteger(v))
|
|
109
|
+
return `${at} must be an integer (${d.kind})`;
|
|
110
|
+
const [lo, hi] = INT_RANGE[d.kind];
|
|
111
|
+
return v < lo || v > hi ? `${at} out of range for ${d.kind}: ${v}` : null;
|
|
112
|
+
}
|
|
113
|
+
case "f32":
|
|
114
|
+
case "f64":
|
|
115
|
+
return typeof v === "number" ? null : `${at} must be a number`;
|
|
116
|
+
case "str":
|
|
117
|
+
case "ref": {
|
|
118
|
+
if (typeof v !== "string") return `${at} must be a string`;
|
|
119
|
+
const max = d.kind === "str" ? d.max : ID_MAX_BYTES;
|
|
120
|
+
const n = utf8Length(v);
|
|
121
|
+
return n > max ? `${at} exceeds ${max} UTF-8 bytes (${n})` : null;
|
|
122
|
+
}
|
|
123
|
+
case "enum":
|
|
124
|
+
return typeof v === "string" && d.values.includes(v) ? null : `${at} must be one of ${d.values.join("|")}`;
|
|
125
|
+
case "list": {
|
|
126
|
+
if (!Array.isArray(v)) return `${at} must be an array`;
|
|
127
|
+
if (v.length > d.max) return `${at} exceeds max length ${d.max} (${v.length})`;
|
|
128
|
+
for (let i = 0; i < v.length; i++) {
|
|
129
|
+
const e = validateValue(d.item, v[i], `${at}[${i}]`);
|
|
130
|
+
if (e) return e;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
case "struct": {
|
|
135
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return `${at} must be an object`;
|
|
136
|
+
for (const [k, fd] of Object.entries(d.fields)) {
|
|
137
|
+
const e = validateValue(fd, v[k], `${at}.${k}`);
|
|
138
|
+
if (e) return e;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function defaultValue(d) {
|
|
145
|
+
if (d.hasDefault) return cloneValue(d.default);
|
|
146
|
+
if (d.opt) return void 0;
|
|
147
|
+
switch (d.kind) {
|
|
148
|
+
case "bool":
|
|
149
|
+
return false;
|
|
150
|
+
case "u8":
|
|
151
|
+
case "u16":
|
|
152
|
+
case "u32":
|
|
153
|
+
case "i32":
|
|
154
|
+
case "f32":
|
|
155
|
+
case "f64":
|
|
156
|
+
return 0;
|
|
157
|
+
case "str":
|
|
158
|
+
case "ref":
|
|
159
|
+
return "";
|
|
160
|
+
case "enum":
|
|
161
|
+
return d.values[0];
|
|
162
|
+
case "list":
|
|
163
|
+
return [];
|
|
164
|
+
case "struct": {
|
|
165
|
+
const o = {};
|
|
166
|
+
for (const [k, fd] of Object.entries(d.fields)) o[k] = defaultValue(fd);
|
|
167
|
+
return o;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function cloneValue(v) {
|
|
172
|
+
if (Array.isArray(v)) return v.map(cloneValue);
|
|
173
|
+
if (v !== null && typeof v === "object") {
|
|
174
|
+
const o = {};
|
|
175
|
+
for (const k of Object.keys(v)) o[k] = cloneValue(v[k]);
|
|
176
|
+
return o;
|
|
177
|
+
}
|
|
178
|
+
return v;
|
|
179
|
+
}
|
|
180
|
+
function normalizeValue(d, v, path = "") {
|
|
181
|
+
const at = path || "value";
|
|
182
|
+
if (v === void 0) {
|
|
183
|
+
if (d.hasDefault) return cloneValue(d.default);
|
|
184
|
+
if (d.opt) return void 0;
|
|
185
|
+
throw new Error(`${at} is required`);
|
|
186
|
+
}
|
|
187
|
+
switch (d.kind) {
|
|
188
|
+
case "f32": {
|
|
189
|
+
if (typeof v !== "number") throw new Error(`${at} must be a number`);
|
|
190
|
+
return Math.fround(v);
|
|
191
|
+
}
|
|
192
|
+
case "list": {
|
|
193
|
+
if (!Array.isArray(v)) throw new Error(`${at} must be an array`);
|
|
194
|
+
if (v.length > d.max) throw new Error(`${at} exceeds max length ${d.max} (${v.length})`);
|
|
195
|
+
return v.map((x, i) => normalizeValue(d.item, x, `${at}[${i}]`));
|
|
196
|
+
}
|
|
197
|
+
case "struct": {
|
|
198
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
199
|
+
throw new Error(`${at} must be an object`);
|
|
200
|
+
const o = {};
|
|
201
|
+
for (const [k, fd] of Object.entries(d.fields)) {
|
|
202
|
+
o[k] = normalizeValue(fd, v[k], `${at}.${k}`);
|
|
203
|
+
}
|
|
204
|
+
return o;
|
|
205
|
+
}
|
|
206
|
+
default: {
|
|
207
|
+
const err = validateValue(d, v, path);
|
|
208
|
+
if (err) throw new Error(err);
|
|
209
|
+
return v;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/sha256.ts
|
|
215
|
+
var K = new Uint32Array([
|
|
216
|
+
1116352408,
|
|
217
|
+
1899447441,
|
|
218
|
+
3049323471,
|
|
219
|
+
3921009573,
|
|
220
|
+
961987163,
|
|
221
|
+
1508970993,
|
|
222
|
+
2453635748,
|
|
223
|
+
2870763221,
|
|
224
|
+
3624381080,
|
|
225
|
+
310598401,
|
|
226
|
+
607225278,
|
|
227
|
+
1426881987,
|
|
228
|
+
1925078388,
|
|
229
|
+
2162078206,
|
|
230
|
+
2614888103,
|
|
231
|
+
3248222580,
|
|
232
|
+
3835390401,
|
|
233
|
+
4022224774,
|
|
234
|
+
264347078,
|
|
235
|
+
604807628,
|
|
236
|
+
770255983,
|
|
237
|
+
1249150122,
|
|
238
|
+
1555081692,
|
|
239
|
+
1996064986,
|
|
240
|
+
2554220882,
|
|
241
|
+
2821834349,
|
|
242
|
+
2952996808,
|
|
243
|
+
3210313671,
|
|
244
|
+
3336571891,
|
|
245
|
+
3584528711,
|
|
246
|
+
113926993,
|
|
247
|
+
338241895,
|
|
248
|
+
666307205,
|
|
249
|
+
773529912,
|
|
250
|
+
1294757372,
|
|
251
|
+
1396182291,
|
|
252
|
+
1695183700,
|
|
253
|
+
1986661051,
|
|
254
|
+
2177026350,
|
|
255
|
+
2456956037,
|
|
256
|
+
2730485921,
|
|
257
|
+
2820302411,
|
|
258
|
+
3259730800,
|
|
259
|
+
3345764771,
|
|
260
|
+
3516065817,
|
|
261
|
+
3600352804,
|
|
262
|
+
4094571909,
|
|
263
|
+
275423344,
|
|
264
|
+
430227734,
|
|
265
|
+
506948616,
|
|
266
|
+
659060556,
|
|
267
|
+
883997877,
|
|
268
|
+
958139571,
|
|
269
|
+
1322822218,
|
|
270
|
+
1537002063,
|
|
271
|
+
1747873779,
|
|
272
|
+
1955562222,
|
|
273
|
+
2024104815,
|
|
274
|
+
2227730452,
|
|
275
|
+
2361852424,
|
|
276
|
+
2428436474,
|
|
277
|
+
2756734187,
|
|
278
|
+
3204031479,
|
|
279
|
+
3329325298
|
|
280
|
+
]);
|
|
281
|
+
function sha256(data) {
|
|
282
|
+
const len = data.length;
|
|
283
|
+
const bitLenHi = Math.floor(len * 8 / 4294967296);
|
|
284
|
+
const bitLenLo = len * 8 >>> 0;
|
|
285
|
+
const padded = new Uint8Array(len + 9 + 63 >> 6 << 6);
|
|
286
|
+
padded.set(data);
|
|
287
|
+
padded[len] = 128;
|
|
288
|
+
const dv = new DataView(padded.buffer);
|
|
289
|
+
dv.setUint32(padded.length - 8, bitLenHi);
|
|
290
|
+
dv.setUint32(padded.length - 4, bitLenLo);
|
|
291
|
+
const h = new Uint32Array([
|
|
292
|
+
1779033703,
|
|
293
|
+
3144134277,
|
|
294
|
+
1013904242,
|
|
295
|
+
2773480762,
|
|
296
|
+
1359893119,
|
|
297
|
+
2600822924,
|
|
298
|
+
528734635,
|
|
299
|
+
1541459225
|
|
300
|
+
]);
|
|
301
|
+
const w = new Uint32Array(64);
|
|
302
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
303
|
+
for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
|
|
304
|
+
for (let i = 16; i < 64; i++) {
|
|
305
|
+
const w15 = w[i - 15];
|
|
306
|
+
const w2 = w[i - 2];
|
|
307
|
+
const s0 = (w15 >>> 7 | w15 << 25) ^ (w15 >>> 18 | w15 << 14) ^ w15 >>> 3;
|
|
308
|
+
const s1 = (w2 >>> 17 | w2 << 15) ^ (w2 >>> 19 | w2 << 13) ^ w2 >>> 10;
|
|
309
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
310
|
+
}
|
|
311
|
+
let a = h[0];
|
|
312
|
+
let b = h[1];
|
|
313
|
+
let c = h[2];
|
|
314
|
+
let d = h[3];
|
|
315
|
+
let e = h[4];
|
|
316
|
+
let f = h[5];
|
|
317
|
+
let g = h[6];
|
|
318
|
+
let hh = h[7];
|
|
319
|
+
for (let i = 0; i < 64; i++) {
|
|
320
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
321
|
+
const ch = e & f ^ ~e & g;
|
|
322
|
+
const t1 = hh + S1 + ch + K[i] + w[i] >>> 0;
|
|
323
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
324
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
325
|
+
const t2 = S0 + maj >>> 0;
|
|
326
|
+
hh = g;
|
|
327
|
+
g = f;
|
|
328
|
+
f = e;
|
|
329
|
+
e = d + t1 >>> 0;
|
|
330
|
+
d = c;
|
|
331
|
+
c = b;
|
|
332
|
+
b = a;
|
|
333
|
+
a = t1 + t2 >>> 0;
|
|
334
|
+
}
|
|
335
|
+
h[0] = h[0] + a >>> 0;
|
|
336
|
+
h[1] = h[1] + b >>> 0;
|
|
337
|
+
h[2] = h[2] + c >>> 0;
|
|
338
|
+
h[3] = h[3] + d >>> 0;
|
|
339
|
+
h[4] = h[4] + e >>> 0;
|
|
340
|
+
h[5] = h[5] + f >>> 0;
|
|
341
|
+
h[6] = h[6] + g >>> 0;
|
|
342
|
+
h[7] = h[7] + hh >>> 0;
|
|
343
|
+
}
|
|
344
|
+
const out = new Uint8Array(32);
|
|
345
|
+
const odv = new DataView(out.buffer);
|
|
346
|
+
for (let i = 0; i < 8; i++) odv.setUint32(i * 4, h[i]);
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
function sha256Hex(data) {
|
|
350
|
+
return toHex(sha256(data));
|
|
351
|
+
}
|
|
352
|
+
function toHex(bytes) {
|
|
353
|
+
let s = "";
|
|
354
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, "0");
|
|
355
|
+
return s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/schema.ts
|
|
359
|
+
function entity(fields, options) {
|
|
360
|
+
checkFields(fields);
|
|
361
|
+
return { kind: "entity", fields, options: options ?? {} };
|
|
362
|
+
}
|
|
363
|
+
function singleton(fields, options) {
|
|
364
|
+
checkFields(fields);
|
|
365
|
+
return { kind: "singleton", fields, options: options ?? {} };
|
|
366
|
+
}
|
|
367
|
+
function checkFields(fields) {
|
|
368
|
+
const names = Object.keys(fields);
|
|
369
|
+
if (names.length === 0) throw new Error("entity/singleton needs at least one field");
|
|
370
|
+
if (names.length > 255) throw new Error("at most 255 fields per entity/singleton");
|
|
371
|
+
for (const n of names) assertFieldName(n);
|
|
372
|
+
}
|
|
373
|
+
function server(spec = {}) {
|
|
374
|
+
return makeRpc("server", spec);
|
|
375
|
+
}
|
|
376
|
+
function client(spec = {}) {
|
|
377
|
+
return makeRpc("client", spec);
|
|
378
|
+
}
|
|
379
|
+
function makeRpc(direction, spec) {
|
|
380
|
+
const params = spec.params ?? {};
|
|
381
|
+
for (const n of Object.keys(params)) assertFieldName(n);
|
|
382
|
+
if (spec.returns) for (const n of Object.keys(spec.returns)) assertFieldName(n);
|
|
383
|
+
return { kind: "rpc", direction, params, returns: spec.returns };
|
|
384
|
+
}
|
|
385
|
+
var RESERVED_RPC_NAMES = ["requestOwnership"];
|
|
386
|
+
var RESERVED_COLLECTION_NAMES = ["clients"];
|
|
387
|
+
function defineSchema(defs, options = {}) {
|
|
388
|
+
const names = Object.keys(defs);
|
|
389
|
+
for (const n of names) {
|
|
390
|
+
assertFieldName(n);
|
|
391
|
+
if (RESERVED_COLLECTION_NAMES.includes(n) && !options.allowReservedNames) {
|
|
392
|
+
throw new Error(`collection name ${JSON.stringify(n)} is reserved by irtio (built-in state)`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const collections = [...names].sort().map((name, index) => {
|
|
396
|
+
const def = defs[name];
|
|
397
|
+
const fields = compileFields(def.fields);
|
|
398
|
+
const o = def.options;
|
|
399
|
+
return {
|
|
400
|
+
name,
|
|
401
|
+
index,
|
|
402
|
+
kind: def.kind,
|
|
403
|
+
fields,
|
|
404
|
+
fieldIndex: new Map(fields.map((f) => [f.name, f.index])),
|
|
405
|
+
serverOwned: o.serverOwned === true,
|
|
406
|
+
visibility: o.visibility ?? "all",
|
|
407
|
+
roles: o.roles ? [...o.roles] : void 0
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
const entityNames = new Set(collections.filter((c) => c.kind === "entity").map((c) => c.name));
|
|
411
|
+
for (const c of collections) {
|
|
412
|
+
for (const f of c.fields) checkRefs(f.type, entityNames, `${c.name}.${f.name}`);
|
|
413
|
+
}
|
|
414
|
+
const rpcMap = options.rpc ?? {};
|
|
415
|
+
for (const n of Object.keys(rpcMap)) {
|
|
416
|
+
assertFieldName(n);
|
|
417
|
+
if (RESERVED_RPC_NAMES.includes(n)) {
|
|
418
|
+
throw new Error(`RPC name ${JSON.stringify(n)} is reserved by irtio (built-in RPC)`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const rpcs = Object.keys(rpcMap).sort().map((name, index) => {
|
|
422
|
+
const r = rpcMap[name];
|
|
423
|
+
for (const f of Object.values(r.params))
|
|
424
|
+
checkRefs(f.desc, entityNames, `rpc ${name} params`);
|
|
425
|
+
if (r.returns)
|
|
426
|
+
for (const f of Object.values(r.returns))
|
|
427
|
+
checkRefs(f.desc, entityNames, `rpc ${name} returns`);
|
|
428
|
+
return {
|
|
429
|
+
name,
|
|
430
|
+
index,
|
|
431
|
+
direction: r.direction,
|
|
432
|
+
params: compileFields(r.params),
|
|
433
|
+
returns: r.returns ? compileFields(r.returns) : void 0
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
const roles = options.roles ?? [];
|
|
437
|
+
if (new Set(roles).size !== roles.length) throw new Error("roles must be unique");
|
|
438
|
+
const canonical = canonicalize({ collections, rpcs, roles });
|
|
439
|
+
const hashBytes = sha256(new TextEncoder().encode(canonical));
|
|
440
|
+
const byName = new Map(collections.map((c) => [c.name, c]));
|
|
441
|
+
return {
|
|
442
|
+
defs,
|
|
443
|
+
rpc: rpcMap,
|
|
444
|
+
roles,
|
|
445
|
+
project: options.project,
|
|
446
|
+
collections,
|
|
447
|
+
collection: (name) => {
|
|
448
|
+
const c = byName.get(name);
|
|
449
|
+
if (!c) throw new Error(`unknown collection ${JSON.stringify(name)}`);
|
|
450
|
+
return c;
|
|
451
|
+
},
|
|
452
|
+
rpcs,
|
|
453
|
+
canonical,
|
|
454
|
+
hash: toHex(hashBytes),
|
|
455
|
+
hash8: hashBytes.slice(0, 8)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function compileFields(fields) {
|
|
459
|
+
return Object.entries(fields).map(([name, t], index) => ({ name, index, type: t.desc }));
|
|
460
|
+
}
|
|
461
|
+
function checkRefs(d, entities, at) {
|
|
462
|
+
switch (d.kind) {
|
|
463
|
+
case "ref":
|
|
464
|
+
if (!entities.has(d.entity))
|
|
465
|
+
throw new Error(`${at}: ref(${JSON.stringify(d.entity)}) does not name an entity`);
|
|
466
|
+
return;
|
|
467
|
+
case "list":
|
|
468
|
+
checkRefs(d.item, entities, at);
|
|
469
|
+
return;
|
|
470
|
+
case "struct":
|
|
471
|
+
for (const [k, fd] of Object.entries(d.fields)) checkRefs(fd, entities, `${at}.${k}`);
|
|
472
|
+
return;
|
|
473
|
+
default:
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
var CANONICAL_VERSION = 1;
|
|
478
|
+
function canonicalType(d) {
|
|
479
|
+
const o = { kind: d.kind };
|
|
480
|
+
if (d.opt) o.opt = true;
|
|
481
|
+
if (d.hasDefault) o.default = d.default;
|
|
482
|
+
switch (d.kind) {
|
|
483
|
+
case "str":
|
|
484
|
+
o.max = d.max;
|
|
485
|
+
break;
|
|
486
|
+
case "enum":
|
|
487
|
+
o.values = [...d.values];
|
|
488
|
+
break;
|
|
489
|
+
case "ref":
|
|
490
|
+
o.entity = d.entity;
|
|
491
|
+
break;
|
|
492
|
+
case "list":
|
|
493
|
+
o.item = canonicalType(d.item);
|
|
494
|
+
o.max = d.max;
|
|
495
|
+
break;
|
|
496
|
+
case "struct":
|
|
497
|
+
o.fields = Object.entries(d.fields).map(([name, t]) => ({ name, type: canonicalType(t) }));
|
|
498
|
+
break;
|
|
499
|
+
default:
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
return o;
|
|
503
|
+
}
|
|
504
|
+
function canonicalFields(fields) {
|
|
505
|
+
return fields.map((f) => ({ name: f.name, type: canonicalType(f.type) }));
|
|
506
|
+
}
|
|
507
|
+
function canonicalize(s) {
|
|
508
|
+
const doc = {
|
|
509
|
+
canonicalVersion: CANONICAL_VERSION,
|
|
510
|
+
entities: s.collections.map((c) => ({
|
|
511
|
+
name: c.name,
|
|
512
|
+
kind: c.kind,
|
|
513
|
+
fields: canonicalFields(c.fields),
|
|
514
|
+
serverOwned: c.serverOwned,
|
|
515
|
+
visibility: c.visibility,
|
|
516
|
+
roles: c.roles ? [...c.roles] : null
|
|
517
|
+
})),
|
|
518
|
+
rpcs: s.rpcs.map((r) => ({
|
|
519
|
+
name: r.name,
|
|
520
|
+
direction: r.direction,
|
|
521
|
+
params: canonicalFields(r.params),
|
|
522
|
+
returns: r.returns ? canonicalFields(r.returns) : null
|
|
523
|
+
})),
|
|
524
|
+
roles: [...s.roles]
|
|
525
|
+
};
|
|
526
|
+
return stableStringify(doc);
|
|
527
|
+
}
|
|
528
|
+
function stableStringify(v) {
|
|
529
|
+
return stringifyValue(v) ?? "null";
|
|
530
|
+
}
|
|
531
|
+
function stringifyValue(v) {
|
|
532
|
+
if (Array.isArray(v)) return `[${v.map((x) => stringifyValue(x) ?? "null").join(",")}]`;
|
|
533
|
+
if (v !== null && typeof v === "object") {
|
|
534
|
+
const keys = Object.keys(v).sort();
|
|
535
|
+
const parts = [];
|
|
536
|
+
for (const k of keys) {
|
|
537
|
+
const s = stringifyValue(v[k]);
|
|
538
|
+
if (s !== void 0) parts.push(`${JSON.stringify(k)}:${s}`);
|
|
539
|
+
}
|
|
540
|
+
return `{${parts.join(",")}}`;
|
|
541
|
+
}
|
|
542
|
+
return JSON.stringify(v);
|
|
543
|
+
}
|
|
544
|
+
function validateForDeploy(schema) {
|
|
545
|
+
const issues = [];
|
|
546
|
+
for (const c of schema.collections) {
|
|
547
|
+
if (c.visibility === "spatial-grid") {
|
|
548
|
+
issues.push({
|
|
549
|
+
level: "error",
|
|
550
|
+
message: `${c.name}: visibility 'spatial-grid' is not yet supported; use 'all' or 'role'`
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (c.visibility === "role") {
|
|
554
|
+
if (!c.roles || c.roles.length === 0) {
|
|
555
|
+
issues.push({
|
|
556
|
+
level: "warning",
|
|
557
|
+
message: `${c.name}: visibility 'role' without roles \u2014 visible to no client role (likely a mistake)`
|
|
558
|
+
});
|
|
559
|
+
} else {
|
|
560
|
+
for (const r of c.roles) {
|
|
561
|
+
if (!schema.roles.includes(r)) {
|
|
562
|
+
issues.push({
|
|
563
|
+
level: "error",
|
|
564
|
+
message: `${c.name}: visibility role ${JSON.stringify(r)} is not in schema roles`
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return issues;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/from-canonical.ts
|
|
575
|
+
var SCALARS = {
|
|
576
|
+
bool,
|
|
577
|
+
u8,
|
|
578
|
+
u16,
|
|
579
|
+
u32,
|
|
580
|
+
i32,
|
|
581
|
+
f32,
|
|
582
|
+
f64
|
|
583
|
+
};
|
|
584
|
+
function schemaFromCanonical(json, options = {}) {
|
|
585
|
+
let doc;
|
|
586
|
+
try {
|
|
587
|
+
doc = JSON.parse(json);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
throw new Error(`schemaFromCanonical: not JSON: ${String(err)}`);
|
|
590
|
+
}
|
|
591
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
592
|
+
throw new Error("schemaFromCanonical: canonical form must be a JSON object");
|
|
593
|
+
}
|
|
594
|
+
const d = doc;
|
|
595
|
+
const version = d.canonicalVersion;
|
|
596
|
+
if (version !== CANONICAL_VERSION) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`schemaFromCanonical: canonicalVersion ${String(version)} is not supported (this build reads ${CANONICAL_VERSION})`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const defs = {};
|
|
602
|
+
for (const e of arrayAt(d, "entities")) {
|
|
603
|
+
const name = stringAt(e, "name");
|
|
604
|
+
const kind = stringAt(e, "kind");
|
|
605
|
+
const fields = fieldsOf(arrayAt(e, "fields"), `${name}`);
|
|
606
|
+
const optionsForDef = entityOptions(e, name);
|
|
607
|
+
if (kind === "entity") defs[name] = entity(fields, optionsForDef);
|
|
608
|
+
else if (kind === "singleton") defs[name] = singleton(fields, optionsForDef);
|
|
609
|
+
else throw new Error(`schemaFromCanonical: ${name}: unknown collection kind ${kind}`);
|
|
610
|
+
}
|
|
611
|
+
const rpc = {};
|
|
612
|
+
for (const r of arrayAt(d, "rpcs")) {
|
|
613
|
+
const name = stringAt(r, "name");
|
|
614
|
+
const direction = stringAt(r, "direction");
|
|
615
|
+
if (direction !== "server" && direction !== "client") {
|
|
616
|
+
throw new Error(`schemaFromCanonical: rpc ${name}: unknown direction ${direction}`);
|
|
617
|
+
}
|
|
618
|
+
const params = fieldsOf(arrayAt(r, "params"), `rpc ${name} params`);
|
|
619
|
+
const returnsRaw = r.returns;
|
|
620
|
+
const returns = returnsRaw === null || returnsRaw === void 0 ? void 0 : fieldsOf(arrayAt(r, "returns"), `rpc ${name} returns`);
|
|
621
|
+
rpc[name] = {
|
|
622
|
+
kind: "rpc",
|
|
623
|
+
direction,
|
|
624
|
+
params,
|
|
625
|
+
returns
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const roles = [];
|
|
629
|
+
for (const role of arrayAtRaw(d, "roles")) {
|
|
630
|
+
if (typeof role !== "string") throw new Error("schemaFromCanonical: roles must be strings");
|
|
631
|
+
roles.push(role);
|
|
632
|
+
}
|
|
633
|
+
const schema = defineSchema(defs, {
|
|
634
|
+
rpc,
|
|
635
|
+
roles,
|
|
636
|
+
// A canonical form produced by `withBuiltins` names the reserved `clients` collection; a
|
|
637
|
+
// reader must be able to replay whatever it is handed.
|
|
638
|
+
allowReservedNames: true,
|
|
639
|
+
...options.project !== void 0 ? { project: options.project } : {}
|
|
640
|
+
});
|
|
641
|
+
const expected = stableStringify(doc);
|
|
642
|
+
if (schema.canonical !== expected) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
"schemaFromCanonical: reconstructed schema does not match its canonical form \u2014 the deployment record is corrupt or was written by a newer irtio"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
return schema;
|
|
648
|
+
}
|
|
649
|
+
function fieldsOf(entries, at) {
|
|
650
|
+
const out = {};
|
|
651
|
+
for (const f of entries) {
|
|
652
|
+
const name = stringAt(f, "name");
|
|
653
|
+
out[name] = typeOf(objectAt(f, "type"), `${at}.${name}`);
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function typeOf(t, at) {
|
|
658
|
+
const kind = stringAt(t, "kind");
|
|
659
|
+
let base;
|
|
660
|
+
switch (kind) {
|
|
661
|
+
case "bool":
|
|
662
|
+
case "u8":
|
|
663
|
+
case "u16":
|
|
664
|
+
case "u32":
|
|
665
|
+
case "i32":
|
|
666
|
+
case "f32":
|
|
667
|
+
case "f64":
|
|
668
|
+
base = SCALARS[kind];
|
|
669
|
+
break;
|
|
670
|
+
case "str":
|
|
671
|
+
base = str(numberAt(t, "max"));
|
|
672
|
+
break;
|
|
673
|
+
case "enum": {
|
|
674
|
+
const values = arrayAtRaw(t, "values").map((v) => {
|
|
675
|
+
if (typeof v !== "string") throw new Error(`${at}: enum values must be strings`);
|
|
676
|
+
return v;
|
|
677
|
+
});
|
|
678
|
+
if (values.length === 0) throw new Error(`${at}: enum has no values`);
|
|
679
|
+
base = enumOf(...values);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "ref":
|
|
683
|
+
base = ref(stringAt(t, "entity"));
|
|
684
|
+
break;
|
|
685
|
+
case "list":
|
|
686
|
+
base = list(typeOf(objectAt(t, "item"), `${at}[]`), numberAt(t, "max"));
|
|
687
|
+
break;
|
|
688
|
+
case "struct":
|
|
689
|
+
base = struct(fieldsOf(arrayAt(t, "fields"), at));
|
|
690
|
+
break;
|
|
691
|
+
default:
|
|
692
|
+
throw new Error(`${at}: unknown type kind ${JSON.stringify(kind)}`);
|
|
693
|
+
}
|
|
694
|
+
if (t.opt === true) base = base.opt;
|
|
695
|
+
if (Object.hasOwn(t, "default")) base = base.default(t.default);
|
|
696
|
+
return base;
|
|
697
|
+
}
|
|
698
|
+
function entityOptions(e, name) {
|
|
699
|
+
const visibility = stringAt(e, "visibility");
|
|
700
|
+
if (visibility !== "all" && visibility !== "role" && visibility !== "spatial-grid") {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`schemaFromCanonical: ${name}: unknown visibility ${JSON.stringify(visibility)}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const rolesRaw = e.roles;
|
|
706
|
+
const roles = rolesRaw === null || rolesRaw === void 0 ? void 0 : arrayAtRaw(e, "roles").map((r) => {
|
|
707
|
+
if (typeof r !== "string")
|
|
708
|
+
throw new Error(`schemaFromCanonical: ${name}: roles must be strings`);
|
|
709
|
+
return r;
|
|
710
|
+
});
|
|
711
|
+
return {
|
|
712
|
+
serverOwned: e.serverOwned === true,
|
|
713
|
+
visibility,
|
|
714
|
+
...roles !== void 0 ? { roles } : {}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function stringAt(o, key) {
|
|
718
|
+
const v = o[key];
|
|
719
|
+
if (typeof v !== "string") throw new Error(`schemaFromCanonical: ${key} must be a string`);
|
|
720
|
+
return v;
|
|
721
|
+
}
|
|
722
|
+
function numberAt(o, key) {
|
|
723
|
+
const v = o[key];
|
|
724
|
+
if (typeof v !== "number" || !Number.isInteger(v)) {
|
|
725
|
+
throw new Error(`schemaFromCanonical: ${key} must be an integer`);
|
|
726
|
+
}
|
|
727
|
+
return v;
|
|
728
|
+
}
|
|
729
|
+
function objectAt(o, key) {
|
|
730
|
+
const v = o[key];
|
|
731
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
732
|
+
throw new Error(`schemaFromCanonical: ${key} must be an object`);
|
|
733
|
+
}
|
|
734
|
+
return v;
|
|
735
|
+
}
|
|
736
|
+
function arrayAtRaw(o, key) {
|
|
737
|
+
const v = o[key];
|
|
738
|
+
if (!Array.isArray(v)) throw new Error(`schemaFromCanonical: ${key} must be an array`);
|
|
739
|
+
return v;
|
|
740
|
+
}
|
|
741
|
+
function arrayAt(o, key) {
|
|
742
|
+
return arrayAtRaw(o, key).map((v) => {
|
|
743
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
744
|
+
throw new Error(`schemaFromCanonical: ${key} entries must be objects`);
|
|
745
|
+
}
|
|
746
|
+
return v;
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/collection.ts
|
|
751
|
+
var SERVER_OWNER = "";
|
|
752
|
+
var SINGLETON_ID = "";
|
|
753
|
+
function assertId(id) {
|
|
754
|
+
if (typeof id !== "string") throw new Error("entity id must be a string");
|
|
755
|
+
if (utf8Length(id) > ID_MAX_BYTES)
|
|
756
|
+
throw new Error(`entity id exceeds ${ID_MAX_BYTES} UTF-8 bytes: ${JSON.stringify(id)}`);
|
|
757
|
+
}
|
|
758
|
+
function assertOwner(owner) {
|
|
759
|
+
if (typeof owner !== "string")
|
|
760
|
+
throw new Error("owner must be a client id string or SERVER_OWNER");
|
|
761
|
+
if (utf8Length(owner) > ID_MAX_BYTES)
|
|
762
|
+
throw new Error(`owner id exceeds ${ID_MAX_BYTES} UTF-8 bytes: ${JSON.stringify(owner)}`);
|
|
763
|
+
}
|
|
764
|
+
var EntityCollection = class {
|
|
765
|
+
constructor(desc) {
|
|
766
|
+
this.desc = desc;
|
|
767
|
+
}
|
|
768
|
+
desc;
|
|
769
|
+
records = /* @__PURE__ */ new Map();
|
|
770
|
+
add(id, values, options) {
|
|
771
|
+
assertId(id);
|
|
772
|
+
const owner = options?.owner ?? SERVER_OWNER;
|
|
773
|
+
assertOwner(owner);
|
|
774
|
+
const value = normalizeRecord(this.desc, values);
|
|
775
|
+
this.records.set(id, { owner, value });
|
|
776
|
+
return value;
|
|
777
|
+
}
|
|
778
|
+
remove(id) {
|
|
779
|
+
return this.records.delete(id);
|
|
780
|
+
}
|
|
781
|
+
get(id) {
|
|
782
|
+
return this.records.get(id)?.value;
|
|
783
|
+
}
|
|
784
|
+
has(id) {
|
|
785
|
+
return this.records.has(id);
|
|
786
|
+
}
|
|
787
|
+
setOwner(id, owner) {
|
|
788
|
+
const r = this.records.get(id);
|
|
789
|
+
if (!r) throw new Error(`setOwner: no instance ${JSON.stringify(id)} in ${this.desc.name}`);
|
|
790
|
+
assertOwner(owner);
|
|
791
|
+
r.owner = owner;
|
|
792
|
+
}
|
|
793
|
+
ownerOf(id) {
|
|
794
|
+
return this.records.get(id)?.owner;
|
|
795
|
+
}
|
|
796
|
+
get size() {
|
|
797
|
+
return this.records.size;
|
|
798
|
+
}
|
|
799
|
+
ids() {
|
|
800
|
+
return this.records.keys();
|
|
801
|
+
}
|
|
802
|
+
*[Symbol.iterator]() {
|
|
803
|
+
for (const [id, r] of this.records) yield [id, r.value];
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
function normalizeRecord(desc, values) {
|
|
807
|
+
if (typeof values !== "object" || values === null) {
|
|
808
|
+
throw new Error(`${desc.name}: values must be an object`);
|
|
809
|
+
}
|
|
810
|
+
for (const k of Object.keys(values)) {
|
|
811
|
+
if (!desc.fieldIndex.has(k))
|
|
812
|
+
throw new Error(`${desc.name}: unknown field ${JSON.stringify(k)}`);
|
|
813
|
+
}
|
|
814
|
+
const out = {};
|
|
815
|
+
for (const f of desc.fields)
|
|
816
|
+
out[f.name] = normalizeValue(f.type, values[f.name], `${desc.name}.${f.name}`);
|
|
817
|
+
return out;
|
|
818
|
+
}
|
|
819
|
+
function defaultRecord(desc) {
|
|
820
|
+
const out = {};
|
|
821
|
+
for (const f of desc.fields) out[f.name] = defaultValue(f.type);
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function createState(schema, init) {
|
|
825
|
+
const state = {};
|
|
826
|
+
for (const c of schema.collections) {
|
|
827
|
+
if (c.kind === "entity") state[c.name] = new EntityCollection(c);
|
|
828
|
+
else {
|
|
829
|
+
const given = init?.[c.name];
|
|
830
|
+
state[c.name] = given ? normalizeRecord(c, given) : defaultRecord(c);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return state;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/dirty.ts
|
|
837
|
+
function createDirtySet() {
|
|
838
|
+
return /* @__PURE__ */ new Map();
|
|
839
|
+
}
|
|
840
|
+
function createFieldMask() {
|
|
841
|
+
return { fields: /* @__PURE__ */ new Set(), nested: /* @__PURE__ */ new Map() };
|
|
842
|
+
}
|
|
843
|
+
function collectionDirty(dirty, collection) {
|
|
844
|
+
let c = dirty.get(collection);
|
|
845
|
+
if (!c) {
|
|
846
|
+
c = { added: /* @__PURE__ */ new Set(), removed: /* @__PURE__ */ new Set(), updated: /* @__PURE__ */ new Map() };
|
|
847
|
+
dirty.set(collection, c);
|
|
848
|
+
}
|
|
849
|
+
return c;
|
|
850
|
+
}
|
|
851
|
+
function recordDirty(dirty, collection, id) {
|
|
852
|
+
const c = collectionDirty(dirty, collection);
|
|
853
|
+
if (c.added.has(id)) return void 0;
|
|
854
|
+
let r = c.updated.get(id);
|
|
855
|
+
if (!r) {
|
|
856
|
+
r = { mask: createFieldMask(), owner: false };
|
|
857
|
+
c.updated.set(id, r);
|
|
858
|
+
}
|
|
859
|
+
return r;
|
|
860
|
+
}
|
|
861
|
+
function markField(dirty, collection, id, path) {
|
|
862
|
+
const r = recordDirty(dirty, collection, id);
|
|
863
|
+
if (r) markPath(r.mask, path);
|
|
864
|
+
}
|
|
865
|
+
function markPath(mask, path) {
|
|
866
|
+
if (path.length === 0) return;
|
|
867
|
+
let m = mask;
|
|
868
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
869
|
+
const idx = path[i];
|
|
870
|
+
if (isWhole(m, idx)) return;
|
|
871
|
+
m.fields.add(idx);
|
|
872
|
+
let n = m.nested.get(idx);
|
|
873
|
+
if (!n) {
|
|
874
|
+
n = createFieldMask();
|
|
875
|
+
m.nested.set(idx, n);
|
|
876
|
+
}
|
|
877
|
+
m = n;
|
|
878
|
+
}
|
|
879
|
+
const last = path[path.length - 1];
|
|
880
|
+
m.fields.add(last);
|
|
881
|
+
m.nested.delete(last);
|
|
882
|
+
}
|
|
883
|
+
function markOwner(dirty, collection, id) {
|
|
884
|
+
const r = recordDirty(dirty, collection, id);
|
|
885
|
+
if (r) r.owner = true;
|
|
886
|
+
}
|
|
887
|
+
function markAdd(dirty, collection, id) {
|
|
888
|
+
const c = collectionDirty(dirty, collection);
|
|
889
|
+
c.removed.delete(id);
|
|
890
|
+
c.updated.delete(id);
|
|
891
|
+
c.added.add(id);
|
|
892
|
+
}
|
|
893
|
+
function markRemove(dirty, collection, id) {
|
|
894
|
+
const c = collectionDirty(dirty, collection);
|
|
895
|
+
c.added.delete(id);
|
|
896
|
+
c.updated.delete(id);
|
|
897
|
+
c.removed.add(id);
|
|
898
|
+
}
|
|
899
|
+
function isDirtyEmpty(dirty) {
|
|
900
|
+
for (const c of dirty.values()) {
|
|
901
|
+
if (c.added.size || c.removed.size || c.updated.size) return false;
|
|
902
|
+
}
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
function isWhole(mask, idx) {
|
|
906
|
+
return mask.fields.has(idx) && !mask.nested.has(idx);
|
|
907
|
+
}
|
|
908
|
+
function mergeDirty(dst, src) {
|
|
909
|
+
for (const [name, sc] of src) {
|
|
910
|
+
for (const id of sc.removed) markRemove(dst, name, id);
|
|
911
|
+
for (const id of sc.added) markAdd(dst, name, id);
|
|
912
|
+
for (const [id, rd] of sc.updated) {
|
|
913
|
+
const r = recordDirty(dst, name, id);
|
|
914
|
+
if (!r) continue;
|
|
915
|
+
if (rd.owner) r.owner = true;
|
|
916
|
+
mergeMask(r.mask, rd.mask);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function mergeMask(dst, src) {
|
|
921
|
+
for (const idx of src.fields) {
|
|
922
|
+
const n = src.nested.get(idx);
|
|
923
|
+
if (!n) {
|
|
924
|
+
dst.fields.add(idx);
|
|
925
|
+
dst.nested.delete(idx);
|
|
926
|
+
} else if (!isWhole(dst, idx)) {
|
|
927
|
+
dst.fields.add(idx);
|
|
928
|
+
let dn = dst.nested.get(idx);
|
|
929
|
+
if (!dn) {
|
|
930
|
+
dn = createFieldMask();
|
|
931
|
+
dst.nested.set(idx, dn);
|
|
932
|
+
}
|
|
933
|
+
mergeMask(dn, n);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function filterDirty(dirty, keep) {
|
|
938
|
+
const out = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const [name, cd] of dirty) if (keep(name)) out.set(name, cd);
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/bytes.ts
|
|
944
|
+
var encoder = new TextEncoder();
|
|
945
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
946
|
+
var ByteWriter = class {
|
|
947
|
+
buf;
|
|
948
|
+
view;
|
|
949
|
+
pos = 0;
|
|
950
|
+
constructor(initialCapacity = 256) {
|
|
951
|
+
this.buf = new Uint8Array(initialCapacity);
|
|
952
|
+
this.view = new DataView(this.buf.buffer);
|
|
953
|
+
}
|
|
954
|
+
get length() {
|
|
955
|
+
return this.pos;
|
|
956
|
+
}
|
|
957
|
+
ensure(n) {
|
|
958
|
+
if (this.pos + n <= this.buf.length) return;
|
|
959
|
+
let cap = this.buf.length * 2;
|
|
960
|
+
while (cap < this.pos + n) cap *= 2;
|
|
961
|
+
const next = new Uint8Array(cap);
|
|
962
|
+
next.set(this.buf.subarray(0, this.pos));
|
|
963
|
+
this.buf = next;
|
|
964
|
+
this.view = new DataView(next.buffer);
|
|
965
|
+
}
|
|
966
|
+
u8(v) {
|
|
967
|
+
this.ensure(1);
|
|
968
|
+
this.buf[this.pos++] = v;
|
|
969
|
+
}
|
|
970
|
+
bool(v) {
|
|
971
|
+
this.u8(v ? 1 : 0);
|
|
972
|
+
}
|
|
973
|
+
u16(v) {
|
|
974
|
+
this.ensure(2);
|
|
975
|
+
this.view.setUint16(this.pos, v, true);
|
|
976
|
+
this.pos += 2;
|
|
977
|
+
}
|
|
978
|
+
u32(v) {
|
|
979
|
+
this.ensure(4);
|
|
980
|
+
this.view.setUint32(this.pos, v, true);
|
|
981
|
+
this.pos += 4;
|
|
982
|
+
}
|
|
983
|
+
i32(v) {
|
|
984
|
+
this.ensure(4);
|
|
985
|
+
this.view.setInt32(this.pos, v, true);
|
|
986
|
+
this.pos += 4;
|
|
987
|
+
}
|
|
988
|
+
f32(v) {
|
|
989
|
+
this.ensure(4);
|
|
990
|
+
this.view.setFloat32(this.pos, v, true);
|
|
991
|
+
this.pos += 4;
|
|
992
|
+
}
|
|
993
|
+
f64(v) {
|
|
994
|
+
this.ensure(8);
|
|
995
|
+
this.view.setFloat64(this.pos, v, true);
|
|
996
|
+
this.pos += 8;
|
|
997
|
+
}
|
|
998
|
+
/** LEB128 unsigned varint (0 .. 2^32-1). */
|
|
999
|
+
varint(v) {
|
|
1000
|
+
if (!Number.isInteger(v) || v < 0 || v > 4294967295)
|
|
1001
|
+
throw new Error(`varint out of range: ${v}`);
|
|
1002
|
+
this.ensure(5);
|
|
1003
|
+
while (v >= 128) {
|
|
1004
|
+
this.buf[this.pos++] = v & 127 | 128;
|
|
1005
|
+
v >>>= 7;
|
|
1006
|
+
}
|
|
1007
|
+
this.buf[this.pos++] = v;
|
|
1008
|
+
}
|
|
1009
|
+
/** Raw bytes, no length prefix. */
|
|
1010
|
+
bytes(b) {
|
|
1011
|
+
this.ensure(b.length);
|
|
1012
|
+
this.buf.set(b, this.pos);
|
|
1013
|
+
this.pos += b.length;
|
|
1014
|
+
}
|
|
1015
|
+
/** varint length + raw bytes. */
|
|
1016
|
+
blob(b) {
|
|
1017
|
+
this.varint(b.length);
|
|
1018
|
+
this.bytes(b);
|
|
1019
|
+
}
|
|
1020
|
+
/** varint UTF-8 byte length + bytes. Returns the byte length written. */
|
|
1021
|
+
str(s) {
|
|
1022
|
+
const b = encoder.encode(s);
|
|
1023
|
+
this.blob(b);
|
|
1024
|
+
return b.length;
|
|
1025
|
+
}
|
|
1026
|
+
/** Reserves `n` bytes and returns their offset (for masks filled in later). */
|
|
1027
|
+
reserve(n) {
|
|
1028
|
+
this.ensure(n);
|
|
1029
|
+
const at = this.pos;
|
|
1030
|
+
this.buf.fill(0, at, at + n);
|
|
1031
|
+
this.pos += n;
|
|
1032
|
+
return at;
|
|
1033
|
+
}
|
|
1034
|
+
setU8(at, v) {
|
|
1035
|
+
this.buf[at] = v;
|
|
1036
|
+
}
|
|
1037
|
+
orU8(at, v) {
|
|
1038
|
+
this.buf[at] |= v;
|
|
1039
|
+
}
|
|
1040
|
+
/** A copy of the written bytes. */
|
|
1041
|
+
finish() {
|
|
1042
|
+
return this.buf.slice(0, this.pos);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
var ByteReader = class {
|
|
1046
|
+
constructor(buf, offset = 0) {
|
|
1047
|
+
this.buf = buf;
|
|
1048
|
+
this.view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1049
|
+
this.pos = offset;
|
|
1050
|
+
}
|
|
1051
|
+
buf;
|
|
1052
|
+
view;
|
|
1053
|
+
pos;
|
|
1054
|
+
get remaining() {
|
|
1055
|
+
return this.buf.length - this.pos;
|
|
1056
|
+
}
|
|
1057
|
+
get eof() {
|
|
1058
|
+
return this.pos >= this.buf.length;
|
|
1059
|
+
}
|
|
1060
|
+
need(n) {
|
|
1061
|
+
if (this.pos + n > this.buf.length) throw new Error("unexpected end of buffer");
|
|
1062
|
+
}
|
|
1063
|
+
u8() {
|
|
1064
|
+
this.need(1);
|
|
1065
|
+
return this.buf[this.pos++];
|
|
1066
|
+
}
|
|
1067
|
+
bool() {
|
|
1068
|
+
return this.u8() !== 0;
|
|
1069
|
+
}
|
|
1070
|
+
u16() {
|
|
1071
|
+
this.need(2);
|
|
1072
|
+
const v = this.view.getUint16(this.pos, true);
|
|
1073
|
+
this.pos += 2;
|
|
1074
|
+
return v;
|
|
1075
|
+
}
|
|
1076
|
+
u32() {
|
|
1077
|
+
this.need(4);
|
|
1078
|
+
const v = this.view.getUint32(this.pos, true);
|
|
1079
|
+
this.pos += 4;
|
|
1080
|
+
return v;
|
|
1081
|
+
}
|
|
1082
|
+
i32() {
|
|
1083
|
+
this.need(4);
|
|
1084
|
+
const v = this.view.getInt32(this.pos, true);
|
|
1085
|
+
this.pos += 4;
|
|
1086
|
+
return v;
|
|
1087
|
+
}
|
|
1088
|
+
f32() {
|
|
1089
|
+
this.need(4);
|
|
1090
|
+
const v = this.view.getFloat32(this.pos, true);
|
|
1091
|
+
this.pos += 4;
|
|
1092
|
+
return v;
|
|
1093
|
+
}
|
|
1094
|
+
f64() {
|
|
1095
|
+
this.need(8);
|
|
1096
|
+
const v = this.view.getFloat64(this.pos, true);
|
|
1097
|
+
this.pos += 8;
|
|
1098
|
+
return v;
|
|
1099
|
+
}
|
|
1100
|
+
varint() {
|
|
1101
|
+
let result = 0;
|
|
1102
|
+
let shift = 0;
|
|
1103
|
+
for (; ; ) {
|
|
1104
|
+
const b = this.u8();
|
|
1105
|
+
result += (b & 127) * 2 ** shift;
|
|
1106
|
+
if ((b & 128) === 0) break;
|
|
1107
|
+
shift += 7;
|
|
1108
|
+
if (shift > 35) throw new Error("varint too long");
|
|
1109
|
+
}
|
|
1110
|
+
if (result > 4294967295) throw new Error("varint out of range");
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
/** `n` raw bytes (a view, not a copy). */
|
|
1114
|
+
bytes(n) {
|
|
1115
|
+
this.need(n);
|
|
1116
|
+
const v = this.buf.subarray(this.pos, this.pos + n);
|
|
1117
|
+
this.pos += n;
|
|
1118
|
+
return v;
|
|
1119
|
+
}
|
|
1120
|
+
/** varint length + raw bytes (a view). */
|
|
1121
|
+
blob() {
|
|
1122
|
+
return this.bytes(this.varint());
|
|
1123
|
+
}
|
|
1124
|
+
str() {
|
|
1125
|
+
return decoder.decode(this.blob());
|
|
1126
|
+
}
|
|
1127
|
+
/** The rest of the buffer (a view). */
|
|
1128
|
+
rest() {
|
|
1129
|
+
const v = this.buf.subarray(this.pos);
|
|
1130
|
+
this.pos = this.buf.length;
|
|
1131
|
+
return v;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
function bytesEqual(a, b) {
|
|
1135
|
+
if (a.length !== b.length) return false;
|
|
1136
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/codec.ts
|
|
1141
|
+
function varintSize(v) {
|
|
1142
|
+
let n = 1;
|
|
1143
|
+
let x = v;
|
|
1144
|
+
while (x >= 128) {
|
|
1145
|
+
x = Math.floor(x / 128);
|
|
1146
|
+
n++;
|
|
1147
|
+
}
|
|
1148
|
+
return n;
|
|
1149
|
+
}
|
|
1150
|
+
var CountingSink = class {
|
|
1151
|
+
length = 0;
|
|
1152
|
+
u8() {
|
|
1153
|
+
this.length += 1;
|
|
1154
|
+
}
|
|
1155
|
+
bool() {
|
|
1156
|
+
this.length += 1;
|
|
1157
|
+
}
|
|
1158
|
+
u16() {
|
|
1159
|
+
this.length += 2;
|
|
1160
|
+
}
|
|
1161
|
+
u32() {
|
|
1162
|
+
this.length += 4;
|
|
1163
|
+
}
|
|
1164
|
+
i32() {
|
|
1165
|
+
this.length += 4;
|
|
1166
|
+
}
|
|
1167
|
+
f32() {
|
|
1168
|
+
this.length += 4;
|
|
1169
|
+
}
|
|
1170
|
+
f64() {
|
|
1171
|
+
this.length += 8;
|
|
1172
|
+
}
|
|
1173
|
+
varint(v) {
|
|
1174
|
+
if (!Number.isInteger(v) || v < 0 || v > 4294967295)
|
|
1175
|
+
throw new Error(`varint out of range: ${v}`);
|
|
1176
|
+
this.length += varintSize(v);
|
|
1177
|
+
}
|
|
1178
|
+
bytes(b) {
|
|
1179
|
+
this.length += b.length;
|
|
1180
|
+
}
|
|
1181
|
+
str(s) {
|
|
1182
|
+
const n = utf8Length(s);
|
|
1183
|
+
this.length += varintSize(n) + n;
|
|
1184
|
+
return n;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
var OP_ADD = 0;
|
|
1188
|
+
var OP_UPDATE = 1;
|
|
1189
|
+
var OP_REMOVE = 2;
|
|
1190
|
+
function fail(path, message) {
|
|
1191
|
+
throw new Error(`${path || "value"}: ${message}`);
|
|
1192
|
+
}
|
|
1193
|
+
function structNames(d) {
|
|
1194
|
+
return Object.keys(d.fields);
|
|
1195
|
+
}
|
|
1196
|
+
function writeValue(w, desc, v, path = "value") {
|
|
1197
|
+
let value = v;
|
|
1198
|
+
if (value === void 0 && desc.hasDefault && desc.default !== void 0) value = desc.default;
|
|
1199
|
+
if (desc.opt) {
|
|
1200
|
+
if (value === void 0) {
|
|
1201
|
+
w.u8(0);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
w.u8(1);
|
|
1205
|
+
} else if (value === void 0) {
|
|
1206
|
+
fail(path, "is required");
|
|
1207
|
+
}
|
|
1208
|
+
switch (desc.kind) {
|
|
1209
|
+
case "bool":
|
|
1210
|
+
if (typeof value !== "boolean") fail(path, "must be a boolean");
|
|
1211
|
+
w.bool(value);
|
|
1212
|
+
return;
|
|
1213
|
+
case "u8":
|
|
1214
|
+
case "u16":
|
|
1215
|
+
case "u32":
|
|
1216
|
+
case "i32": {
|
|
1217
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
1218
|
+
fail(path, `must be an integer (${desc.kind})`);
|
|
1219
|
+
const [lo, hi] = INT_RANGE[desc.kind];
|
|
1220
|
+
if (value < lo || value > hi) fail(path, `out of range for ${desc.kind}: ${value}`);
|
|
1221
|
+
if (desc.kind === "u8") w.u8(value);
|
|
1222
|
+
else if (desc.kind === "u16") w.u16(value);
|
|
1223
|
+
else if (desc.kind === "u32") w.u32(value);
|
|
1224
|
+
else w.i32(value);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
case "f32":
|
|
1228
|
+
if (typeof value !== "number") fail(path, "must be a number");
|
|
1229
|
+
w.f32(value);
|
|
1230
|
+
return;
|
|
1231
|
+
case "f64":
|
|
1232
|
+
if (typeof value !== "number") fail(path, "must be a number");
|
|
1233
|
+
w.f64(value);
|
|
1234
|
+
return;
|
|
1235
|
+
case "str":
|
|
1236
|
+
case "ref": {
|
|
1237
|
+
if (typeof value !== "string") fail(path, "must be a string");
|
|
1238
|
+
const max = desc.kind === "str" ? desc.max : ID_MAX_BYTES;
|
|
1239
|
+
const n = utf8Length(value);
|
|
1240
|
+
if (n > max) fail(path, `exceeds ${max} UTF-8 bytes (${n})`);
|
|
1241
|
+
w.str(value);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
case "enum": {
|
|
1245
|
+
if (typeof value !== "string") fail(path, "must be a string");
|
|
1246
|
+
const i = desc.values.indexOf(value);
|
|
1247
|
+
if (i < 0)
|
|
1248
|
+
fail(path, `must be one of ${desc.values.join("|")} (got ${JSON.stringify(value)})`);
|
|
1249
|
+
w.u8(i);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
case "list": {
|
|
1253
|
+
if (!Array.isArray(value)) fail(path, "must be an array");
|
|
1254
|
+
if (value.length > desc.max) fail(path, `exceeds max length ${desc.max} (${value.length})`);
|
|
1255
|
+
w.varint(value.length);
|
|
1256
|
+
for (let i = 0; i < value.length; i++) writeValue(w, desc.item, value[i], `${path}[${i}]`);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
case "struct": {
|
|
1260
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1261
|
+
fail(path, "must be an object");
|
|
1262
|
+
const o = value;
|
|
1263
|
+
for (const [k, fd] of Object.entries(desc.fields)) writeValue(w, fd, o[k], `${path}.${k}`);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function readValue(r, desc) {
|
|
1269
|
+
if (desc.opt && r.u8() === 0) return void 0;
|
|
1270
|
+
switch (desc.kind) {
|
|
1271
|
+
case "bool":
|
|
1272
|
+
return r.bool();
|
|
1273
|
+
case "u8":
|
|
1274
|
+
return r.u8();
|
|
1275
|
+
case "u16":
|
|
1276
|
+
return r.u16();
|
|
1277
|
+
case "u32":
|
|
1278
|
+
return r.u32();
|
|
1279
|
+
case "i32":
|
|
1280
|
+
return r.i32();
|
|
1281
|
+
case "f32":
|
|
1282
|
+
return r.f32();
|
|
1283
|
+
case "f64":
|
|
1284
|
+
return r.f64();
|
|
1285
|
+
case "str":
|
|
1286
|
+
case "ref":
|
|
1287
|
+
return r.str();
|
|
1288
|
+
case "enum": {
|
|
1289
|
+
const i = r.u8();
|
|
1290
|
+
const v = desc.values[i];
|
|
1291
|
+
if (v === void 0)
|
|
1292
|
+
throw new Error(`unknown enum index ${i} (${desc.values.length} values)`);
|
|
1293
|
+
return v;
|
|
1294
|
+
}
|
|
1295
|
+
case "list": {
|
|
1296
|
+
const n = r.varint();
|
|
1297
|
+
if (n > desc.max) throw new Error(`list exceeds max length ${desc.max} (${n})`);
|
|
1298
|
+
const out = [];
|
|
1299
|
+
for (let i = 0; i < n; i++) out.push(readValue(r, desc.item));
|
|
1300
|
+
return out;
|
|
1301
|
+
}
|
|
1302
|
+
case "struct": {
|
|
1303
|
+
const o = {};
|
|
1304
|
+
for (const [k, fd] of Object.entries(desc.fields)) o[k] = readValue(r, fd);
|
|
1305
|
+
return o;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function writeRecord(w, fields, value, path) {
|
|
1310
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1311
|
+
fail(path, "must be an object");
|
|
1312
|
+
for (const f of fields) writeValue(w, f.type, value[f.name], `${path}.${f.name}`);
|
|
1313
|
+
}
|
|
1314
|
+
function readRecord(r, fields) {
|
|
1315
|
+
const o = {};
|
|
1316
|
+
for (const f of fields) o[f.name] = readValue(r, f.type);
|
|
1317
|
+
return o;
|
|
1318
|
+
}
|
|
1319
|
+
function encodeFields(fields, value) {
|
|
1320
|
+
const w = new ByteWriter(64);
|
|
1321
|
+
writeRecord(w, fields, value, "fields");
|
|
1322
|
+
return w.finish();
|
|
1323
|
+
}
|
|
1324
|
+
function decodeFields(fields, bytes) {
|
|
1325
|
+
const r = bytes instanceof ByteReader ? bytes : new ByteReader(bytes);
|
|
1326
|
+
return readRecord(r, fields);
|
|
1327
|
+
}
|
|
1328
|
+
function entityOf(state, name) {
|
|
1329
|
+
const c = state[name];
|
|
1330
|
+
if (!c || typeof c.get !== "function")
|
|
1331
|
+
throw new Error(`state is missing entity collection ${JSON.stringify(name)}`);
|
|
1332
|
+
return c;
|
|
1333
|
+
}
|
|
1334
|
+
function singletonOf(state, name) {
|
|
1335
|
+
const s = state[name];
|
|
1336
|
+
if (!s || typeof s !== "object")
|
|
1337
|
+
throw new Error(`state is missing singleton ${JSON.stringify(name)}`);
|
|
1338
|
+
return s;
|
|
1339
|
+
}
|
|
1340
|
+
function writeSnapshot(w, schema, state, tick, options) {
|
|
1341
|
+
w.u32(tick);
|
|
1342
|
+
w.bytes(schema.hash8);
|
|
1343
|
+
for (const c of schema.collections) {
|
|
1344
|
+
const visible = options?.collections ? options.collections(c) : true;
|
|
1345
|
+
if (c.kind === "entity") {
|
|
1346
|
+
if (!visible) {
|
|
1347
|
+
w.varint(0);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const coll = entityOf(state, c.name);
|
|
1351
|
+
w.varint(coll.size);
|
|
1352
|
+
for (const id of coll.ids()) {
|
|
1353
|
+
const value = coll.get(id);
|
|
1354
|
+
if (value === void 0) throw new Error(`${c.name}: id ${JSON.stringify(id)} vanished`);
|
|
1355
|
+
w.str(id);
|
|
1356
|
+
w.str(coll.ownerOf(id) ?? "");
|
|
1357
|
+
writeRecord(w, c.fields, value, `${c.name}[${id}]`);
|
|
1358
|
+
}
|
|
1359
|
+
} else {
|
|
1360
|
+
writeRecord(w, c.fields, visible ? singletonOf(state, c.name) : defaultRecord(c), c.name);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function encodeSnapshot(schema, state, header, options) {
|
|
1365
|
+
const w = new ByteWriter(1024);
|
|
1366
|
+
writeSnapshot(w, schema, state, header.tick, options);
|
|
1367
|
+
return w.finish();
|
|
1368
|
+
}
|
|
1369
|
+
function decodeSnapshot(schema, bytes) {
|
|
1370
|
+
const r = new ByteReader(bytes);
|
|
1371
|
+
const tick = r.u32();
|
|
1372
|
+
const hash8 = r.bytes(8).slice();
|
|
1373
|
+
const state = createState(schema);
|
|
1374
|
+
for (const c of schema.collections) {
|
|
1375
|
+
if (c.kind === "entity") {
|
|
1376
|
+
const coll = entityOf(state, c.name);
|
|
1377
|
+
const n = r.varint();
|
|
1378
|
+
for (let i = 0; i < n; i++) {
|
|
1379
|
+
const id = r.str();
|
|
1380
|
+
const owner = r.str();
|
|
1381
|
+
coll.add(id, readRecord(r, c.fields), { owner });
|
|
1382
|
+
}
|
|
1383
|
+
} else {
|
|
1384
|
+
const target = singletonOf(state, c.name);
|
|
1385
|
+
const value = readRecord(r, c.fields);
|
|
1386
|
+
for (const f of c.fields) target[f.name] = value[f.name];
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return { tick, hash8, state };
|
|
1390
|
+
}
|
|
1391
|
+
function planCollection(c, state, dirty) {
|
|
1392
|
+
const cd = dirty.get(c.name);
|
|
1393
|
+
if (!cd) return void 0;
|
|
1394
|
+
const ops = [];
|
|
1395
|
+
if (c.kind === "entity") {
|
|
1396
|
+
const coll = entityOf(state, c.name);
|
|
1397
|
+
for (const id of cd.added) if (coll.has(id)) ops.push({ kind: "add", id });
|
|
1398
|
+
for (const [id, rd] of cd.updated) {
|
|
1399
|
+
if (!rd.owner && rd.mask.fields.size === 0) continue;
|
|
1400
|
+
if (!coll.has(id)) continue;
|
|
1401
|
+
ops.push({ kind: "update", id, mask: rd.mask, owner: rd.owner });
|
|
1402
|
+
}
|
|
1403
|
+
for (const id of cd.removed) ops.push({ kind: "remove", id });
|
|
1404
|
+
} else {
|
|
1405
|
+
for (const [id, rd] of cd.updated) {
|
|
1406
|
+
if (rd.mask.fields.size === 0) continue;
|
|
1407
|
+
ops.push({ kind: "update", id, mask: rd.mask, owner: false });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return ops.length > 0 ? ops : void 0;
|
|
1411
|
+
}
|
|
1412
|
+
function maskBytesFor(indices, bits) {
|
|
1413
|
+
const bytes = new Uint8Array(bits + 7 >> 3);
|
|
1414
|
+
for (const i of indices) bytes[i >> 3] |= 1 << (i & 7);
|
|
1415
|
+
return bytes;
|
|
1416
|
+
}
|
|
1417
|
+
function sortedIndices(mask, limit) {
|
|
1418
|
+
return [...mask.fields].filter((i) => i >= 0 && i < limit).sort((a, b) => a - b);
|
|
1419
|
+
}
|
|
1420
|
+
function writeDirtyField(w, desc, v, nested, path) {
|
|
1421
|
+
if (desc.kind !== "struct") {
|
|
1422
|
+
writeValue(w, desc, v, path);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
let value = v;
|
|
1426
|
+
if (value === void 0 && desc.hasDefault && desc.default !== void 0) value = desc.default;
|
|
1427
|
+
if (desc.opt) {
|
|
1428
|
+
if (value === void 0) {
|
|
1429
|
+
w.u8(0);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
w.u8(1);
|
|
1433
|
+
} else if (value === void 0) {
|
|
1434
|
+
fail(path, "is required");
|
|
1435
|
+
}
|
|
1436
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1437
|
+
fail(path, "must be an object");
|
|
1438
|
+
const o = value;
|
|
1439
|
+
const names = structNames(desc);
|
|
1440
|
+
const indices = nested ? sortedIndices(nested, names.length) : names.map((_, i) => i);
|
|
1441
|
+
w.bytes(maskBytesFor(indices, names.length));
|
|
1442
|
+
for (const i of indices) {
|
|
1443
|
+
const name = names[i];
|
|
1444
|
+
const fd = desc.fields[name];
|
|
1445
|
+
writeDirtyField(w, fd, o[name], nested?.nested.get(i), `${path}.${name}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function writeDelta(w, schema, state, dirty, tick) {
|
|
1449
|
+
w.u32(tick);
|
|
1450
|
+
w.bytes(schema.hash8);
|
|
1451
|
+
const planned = [];
|
|
1452
|
+
for (const c of schema.collections) {
|
|
1453
|
+
const ops = planCollection(c, state, dirty);
|
|
1454
|
+
if (ops) planned.push({ c, ops });
|
|
1455
|
+
}
|
|
1456
|
+
w.varint(planned.length);
|
|
1457
|
+
for (const { c, ops } of planned) {
|
|
1458
|
+
w.varint(c.index);
|
|
1459
|
+
w.varint(ops.length);
|
|
1460
|
+
const coll = c.kind === "entity" ? entityOf(state, c.name) : void 0;
|
|
1461
|
+
for (const op of ops) {
|
|
1462
|
+
if (op.kind === "remove") {
|
|
1463
|
+
w.u8(OP_REMOVE);
|
|
1464
|
+
w.str(op.id);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
const value = coll === void 0 ? singletonOf(state, c.name) : coll.get(op.id);
|
|
1468
|
+
if (op.kind === "add") {
|
|
1469
|
+
w.u8(OP_ADD);
|
|
1470
|
+
w.str(op.id);
|
|
1471
|
+
w.str(coll.ownerOf(op.id) ?? "");
|
|
1472
|
+
writeRecord(w, c.fields, value, `${c.name}[${op.id}]`);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
w.u8(OP_UPDATE);
|
|
1476
|
+
w.str(op.id);
|
|
1477
|
+
const n = c.fields.length;
|
|
1478
|
+
const indices = sortedIndices(op.mask, n);
|
|
1479
|
+
const bits = op.owner ? [...indices, n] : indices;
|
|
1480
|
+
w.bytes(maskBytesFor(bits, n + 1));
|
|
1481
|
+
if (op.owner) w.str(coll.ownerOf(op.id) ?? "");
|
|
1482
|
+
for (const i of indices) {
|
|
1483
|
+
const f = c.fields[i];
|
|
1484
|
+
writeDirtyField(
|
|
1485
|
+
w,
|
|
1486
|
+
f.type,
|
|
1487
|
+
value[f.name],
|
|
1488
|
+
op.mask.nested.get(i),
|
|
1489
|
+
`${c.name}[${op.id}].${f.name}`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
function encodeDelta(schema, state, dirty, header) {
|
|
1496
|
+
const w = new ByteWriter(512);
|
|
1497
|
+
writeDelta(w, schema, state, dirty, header.tick);
|
|
1498
|
+
return w.finish();
|
|
1499
|
+
}
|
|
1500
|
+
function estimateSize(schema, state, dirty) {
|
|
1501
|
+
const w = new CountingSink();
|
|
1502
|
+
writeDelta(w, schema, state, dirty, 0);
|
|
1503
|
+
return w.length;
|
|
1504
|
+
}
|
|
1505
|
+
function readBit(mask, i) {
|
|
1506
|
+
return ((mask[i >> 3] ?? 0) & 1 << (i & 7)) !== 0;
|
|
1507
|
+
}
|
|
1508
|
+
function readDirtyField(r, desc) {
|
|
1509
|
+
if (desc.kind !== "struct") return { value: readValue(r, desc) };
|
|
1510
|
+
if (desc.opt && r.u8() === 0) return { value: void 0 };
|
|
1511
|
+
const names = structNames(desc);
|
|
1512
|
+
const maskBytes = r.bytes(names.length + 7 >> 3);
|
|
1513
|
+
const out = {};
|
|
1514
|
+
const mask = createFieldMask();
|
|
1515
|
+
for (let i = 0; i < names.length; i++) {
|
|
1516
|
+
if (!readBit(maskBytes, i)) continue;
|
|
1517
|
+
const name = names[i];
|
|
1518
|
+
const fd = desc.fields[name];
|
|
1519
|
+
const got = readDirtyField(r, fd);
|
|
1520
|
+
out[name] = got.value;
|
|
1521
|
+
mask.fields.add(i);
|
|
1522
|
+
if (got.nested) mask.nested.set(i, got.nested);
|
|
1523
|
+
}
|
|
1524
|
+
return { value: out, nested: mask };
|
|
1525
|
+
}
|
|
1526
|
+
function decodeDelta(schema, bytes) {
|
|
1527
|
+
return decodeDeltaFrom(schema, new ByteReader(bytes));
|
|
1528
|
+
}
|
|
1529
|
+
function decodeDeltaFrom(schema, r) {
|
|
1530
|
+
const tick = r.u32();
|
|
1531
|
+
const hash8 = r.bytes(8).slice();
|
|
1532
|
+
const collections = [];
|
|
1533
|
+
const count = r.varint();
|
|
1534
|
+
for (let ci = 0; ci < count; ci++) {
|
|
1535
|
+
const index = r.varint();
|
|
1536
|
+
const c = schema.collections[index];
|
|
1537
|
+
if (!c) throw new Error(`delta references unknown collection index ${index}`);
|
|
1538
|
+
const opCount = r.varint();
|
|
1539
|
+
const ops = [];
|
|
1540
|
+
for (let oi = 0; oi < opCount; oi++) {
|
|
1541
|
+
const op = r.u8();
|
|
1542
|
+
if (op === OP_REMOVE) {
|
|
1543
|
+
ops.push({ op: "remove", id: r.str() });
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
if (op === OP_ADD) {
|
|
1547
|
+
const id2 = r.str();
|
|
1548
|
+
const owner2 = r.str();
|
|
1549
|
+
ops.push({ op: "add", id: id2, owner: owner2, value: readRecord(r, c.fields) });
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (op !== OP_UPDATE) throw new Error(`unknown delta op byte ${op} in ${c.name}`);
|
|
1553
|
+
const id = r.str();
|
|
1554
|
+
const n = c.fields.length;
|
|
1555
|
+
const maskBytes = r.bytes(n + 1 + 7 >> 3);
|
|
1556
|
+
const ownerDirty = readBit(maskBytes, n);
|
|
1557
|
+
const owner = ownerDirty ? r.str() : void 0;
|
|
1558
|
+
const mask = createFieldMask();
|
|
1559
|
+
const patch = {};
|
|
1560
|
+
for (let i = 0; i < n; i++) {
|
|
1561
|
+
if (!readBit(maskBytes, i)) continue;
|
|
1562
|
+
const f = c.fields[i];
|
|
1563
|
+
const got = readDirtyField(r, f.type);
|
|
1564
|
+
patch[f.name] = got.value;
|
|
1565
|
+
mask.fields.add(i);
|
|
1566
|
+
if (got.nested) mask.nested.set(i, got.nested);
|
|
1567
|
+
}
|
|
1568
|
+
ops.push(
|
|
1569
|
+
owner === void 0 ? { op: "update", id, mask, patch } : { op: "update", id, owner, mask, patch }
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
collections.push({ name: c.name, ops });
|
|
1573
|
+
}
|
|
1574
|
+
return { tick, hash8, collections };
|
|
1575
|
+
}
|
|
1576
|
+
function applyPatch(fieldTypes, target, patch, mask) {
|
|
1577
|
+
for (const i of sortedIndices(mask, fieldTypes.length)) {
|
|
1578
|
+
const f = fieldTypes[i];
|
|
1579
|
+
const value = patch[f.name];
|
|
1580
|
+
const nested = mask.nested.get(i);
|
|
1581
|
+
if (nested && f.type.kind === "struct" && value !== void 0) {
|
|
1582
|
+
const current = target[f.name];
|
|
1583
|
+
if (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
1584
|
+
const names = structNames(f.type);
|
|
1585
|
+
applyPatch(
|
|
1586
|
+
names.map((name) => ({
|
|
1587
|
+
name,
|
|
1588
|
+
type: f.type.fields[name]
|
|
1589
|
+
})),
|
|
1590
|
+
current,
|
|
1591
|
+
value,
|
|
1592
|
+
nested
|
|
1593
|
+
);
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
target[f.name] = value;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function applyDelta(schema, state, delta) {
|
|
1601
|
+
for (const dc of delta.collections) {
|
|
1602
|
+
const c = schema.collection(dc.name);
|
|
1603
|
+
if (c.kind === "entity") {
|
|
1604
|
+
const coll = entityOf(state, c.name);
|
|
1605
|
+
for (const op of dc.ops) {
|
|
1606
|
+
if (op.op === "add") {
|
|
1607
|
+
coll.add(op.id, op.value, { owner: op.owner });
|
|
1608
|
+
} else if (op.op === "remove") {
|
|
1609
|
+
coll.remove(op.id);
|
|
1610
|
+
} else {
|
|
1611
|
+
if (!coll.has(op.id)) continue;
|
|
1612
|
+
const rec = coll.get(op.id);
|
|
1613
|
+
if (rec) applyPatch(c.fields, rec, op.patch, op.mask);
|
|
1614
|
+
if (op.owner !== void 0) coll.setOwner(op.id, op.owner);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
} else {
|
|
1618
|
+
const target = singletonOf(state, c.name);
|
|
1619
|
+
for (const op of dc.ops) {
|
|
1620
|
+
if (op.op !== "update") continue;
|
|
1621
|
+
applyPatch(c.fields, target, op.patch, op.mask);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function valueEquals(a, b) {
|
|
1627
|
+
if (Object.is(a, b)) return true;
|
|
1628
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1629
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1630
|
+
for (let i = 0; i < a.length; i++) if (!valueEquals(a[i], b[i])) return false;
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
1634
|
+
const ao = a;
|
|
1635
|
+
const bo = b;
|
|
1636
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(ao), ...Object.keys(bo)]);
|
|
1637
|
+
for (const k of keys) if (!valueEquals(ao[k], bo[k])) return false;
|
|
1638
|
+
return true;
|
|
1639
|
+
}
|
|
1640
|
+
function diffField(desc, a, b, mask, idx) {
|
|
1641
|
+
if (desc.kind === "struct" && typeof a === "object" && a !== null && !Array.isArray(a) && typeof b === "object" && b !== null && !Array.isArray(b)) {
|
|
1642
|
+
const nested = createFieldMask();
|
|
1643
|
+
const names = structNames(desc);
|
|
1644
|
+
for (let i = 0; i < names.length; i++) {
|
|
1645
|
+
const name = names[i];
|
|
1646
|
+
diffField(desc.fields[name], a[name], b[name], nested, i);
|
|
1647
|
+
}
|
|
1648
|
+
if (nested.fields.size === 0) return;
|
|
1649
|
+
mask.fields.add(idx);
|
|
1650
|
+
mask.nested.set(idx, nested);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (!valueEquals(a, b)) mask.fields.add(idx);
|
|
1654
|
+
}
|
|
1655
|
+
function diffRecord(c, a, b, mask) {
|
|
1656
|
+
for (const f of c.fields) diffField(f.type, a[f.name], b[f.name], mask, f.index);
|
|
1657
|
+
}
|
|
1658
|
+
function computeDirty(schema, from, to) {
|
|
1659
|
+
const dirty = createDirtySet();
|
|
1660
|
+
for (const c of schema.collections) {
|
|
1661
|
+
if (c.kind === "entity") {
|
|
1662
|
+
const a = entityOf(from, c.name);
|
|
1663
|
+
const b = entityOf(to, c.name);
|
|
1664
|
+
let cd;
|
|
1665
|
+
const ensure = () => {
|
|
1666
|
+
cd ??= collectionDirty(dirty, c.name);
|
|
1667
|
+
return cd;
|
|
1668
|
+
};
|
|
1669
|
+
for (const id of b.ids()) {
|
|
1670
|
+
if (!a.has(id)) {
|
|
1671
|
+
ensure().added.add(id);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const mask = createFieldMask();
|
|
1675
|
+
diffRecord(c, a.get(id), b.get(id), mask);
|
|
1676
|
+
const owner = a.ownerOf(id) !== b.ownerOf(id);
|
|
1677
|
+
if (mask.fields.size > 0 || owner) ensure().updated.set(id, { mask, owner });
|
|
1678
|
+
}
|
|
1679
|
+
for (const id of a.ids()) if (!b.has(id)) ensure().removed.add(id);
|
|
1680
|
+
} else {
|
|
1681
|
+
const mask = createFieldMask();
|
|
1682
|
+
diffRecord(c, singletonOf(from, c.name), singletonOf(to, c.name), mask);
|
|
1683
|
+
if (mask.fields.size > 0) {
|
|
1684
|
+
collectionDirty(dirty, c.name).updated.set(SINGLETON_ID, { mask, owner: false });
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return dirty;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// src/track.ts
|
|
1692
|
+
var fieldTables = /* @__PURE__ */ new WeakMap();
|
|
1693
|
+
function structFields(d) {
|
|
1694
|
+
let t = fieldTables.get(d);
|
|
1695
|
+
if (!t) {
|
|
1696
|
+
const m = /* @__PURE__ */ new Map();
|
|
1697
|
+
const names = Object.keys(d.fields);
|
|
1698
|
+
names.forEach((name, index) => m.set(name, { name, index, type: d.fields[name] }));
|
|
1699
|
+
fieldTables.set(d, m);
|
|
1700
|
+
t = m;
|
|
1701
|
+
}
|
|
1702
|
+
return t;
|
|
1703
|
+
}
|
|
1704
|
+
function collectionFields(c) {
|
|
1705
|
+
let t = fieldTables.get(c);
|
|
1706
|
+
if (!t) {
|
|
1707
|
+
t = new Map(c.fields.map((f) => [f.name, f]));
|
|
1708
|
+
fieldTables.set(c, t);
|
|
1709
|
+
}
|
|
1710
|
+
return t;
|
|
1711
|
+
}
|
|
1712
|
+
function childPath(path, whole, idx) {
|
|
1713
|
+
return whole ? path : [...path, idx];
|
|
1714
|
+
}
|
|
1715
|
+
function mark(root, path) {
|
|
1716
|
+
markField(root.ctx.dirty, root.collection, root.id, path);
|
|
1717
|
+
}
|
|
1718
|
+
function assertLive(root) {
|
|
1719
|
+
if (!root.live) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`${root.collection}: instance ${JSON.stringify(root.id)} was removed; its proxy is stale`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function objHandler(node) {
|
|
1726
|
+
return {
|
|
1727
|
+
get(target, prop, receiver) {
|
|
1728
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
1729
|
+
const f = node.fields.get(prop);
|
|
1730
|
+
if (!f) return Reflect.get(target, prop, receiver);
|
|
1731
|
+
const v = target[f.name];
|
|
1732
|
+
if (v === null || typeof v !== "object") return v;
|
|
1733
|
+
if (f.type.kind === "struct") {
|
|
1734
|
+
return structProxy(node, f, v);
|
|
1735
|
+
}
|
|
1736
|
+
if (f.type.kind === "list") return listProxy(node, f, v);
|
|
1737
|
+
return v;
|
|
1738
|
+
},
|
|
1739
|
+
set(target, prop, value) {
|
|
1740
|
+
if (typeof prop !== "string") {
|
|
1741
|
+
throw new Error(`${node.label}: cannot set ${String(prop)}`);
|
|
1742
|
+
}
|
|
1743
|
+
const f = node.fields.get(prop);
|
|
1744
|
+
if (!f) throw new Error(`${node.label}: unknown field ${JSON.stringify(prop)}`);
|
|
1745
|
+
assertLive(node.root);
|
|
1746
|
+
const at = `${node.label}.${f.name}`;
|
|
1747
|
+
const next = value === void 0 && f.type.opt ? void 0 : normalizeValue(f.type, value, at);
|
|
1748
|
+
if ((next === null || typeof next !== "object") && Object.is(target[f.name], next))
|
|
1749
|
+
return true;
|
|
1750
|
+
target[f.name] = next;
|
|
1751
|
+
mark(node.root, childPath(node.path, node.whole, f.index));
|
|
1752
|
+
return true;
|
|
1753
|
+
},
|
|
1754
|
+
deleteProperty(_target, prop) {
|
|
1755
|
+
throw new Error(
|
|
1756
|
+
`${node.label}: cannot delete ${String(prop)}; set an .opt field to undefined instead`
|
|
1757
|
+
);
|
|
1758
|
+
},
|
|
1759
|
+
defineProperty(_target, prop) {
|
|
1760
|
+
throw new Error(`${node.label}: cannot defineProperty ${String(prop)}; assign instead`);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
function makeObjProxy(node) {
|
|
1765
|
+
const p = new Proxy(node.target, objHandler(node));
|
|
1766
|
+
node.root.ctx.proxies.set(node.target, p);
|
|
1767
|
+
return p;
|
|
1768
|
+
}
|
|
1769
|
+
function structProxy(parent, f, value) {
|
|
1770
|
+
const cached = parent.root.ctx.proxies.get(value);
|
|
1771
|
+
if (cached) return cached;
|
|
1772
|
+
return makeObjProxy({
|
|
1773
|
+
root: parent.root,
|
|
1774
|
+
target: value,
|
|
1775
|
+
fields: structFields(f.type),
|
|
1776
|
+
path: childPath(parent.path, parent.whole, f.index),
|
|
1777
|
+
whole: parent.whole,
|
|
1778
|
+
label: `${parent.label}.${f.name}`
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
var MUTATORS = /* @__PURE__ */ new Set([
|
|
1782
|
+
"push",
|
|
1783
|
+
"pop",
|
|
1784
|
+
"shift",
|
|
1785
|
+
"unshift",
|
|
1786
|
+
"splice",
|
|
1787
|
+
"sort",
|
|
1788
|
+
"reverse",
|
|
1789
|
+
"fill",
|
|
1790
|
+
"copyWithin"
|
|
1791
|
+
]);
|
|
1792
|
+
function isIndexProp(prop) {
|
|
1793
|
+
const c = prop.charCodeAt(0);
|
|
1794
|
+
return c >= 48 && c <= 57 && String(Number(prop)) === prop;
|
|
1795
|
+
}
|
|
1796
|
+
function restoreArray(t, snap) {
|
|
1797
|
+
t.length = snap.length;
|
|
1798
|
+
for (let i = 0; i < snap.length; i++) t[i] = snap[i];
|
|
1799
|
+
}
|
|
1800
|
+
function finishList(node, snap) {
|
|
1801
|
+
const t = node.target;
|
|
1802
|
+
const err = validateValue(node.desc, t, node.label);
|
|
1803
|
+
if (err) {
|
|
1804
|
+
restoreArray(t, snap);
|
|
1805
|
+
throw new Error(err);
|
|
1806
|
+
}
|
|
1807
|
+
const item = node.desc.item;
|
|
1808
|
+
if (item.kind === "struct" || item.kind === "list") {
|
|
1809
|
+
const norm = normalizeValue(node.desc, t, node.label);
|
|
1810
|
+
restoreArray(t, norm);
|
|
1811
|
+
} else if (item.kind === "f32") {
|
|
1812
|
+
for (let i = 0; i < t.length; i++) {
|
|
1813
|
+
if (t[i] !== void 0) t[i] = Math.fround(t[i]);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
mark(node.root, node.path);
|
|
1817
|
+
}
|
|
1818
|
+
function listMethod(node, name) {
|
|
1819
|
+
let fn = node.methods.get(name);
|
|
1820
|
+
if (!fn) {
|
|
1821
|
+
fn = (...args) => {
|
|
1822
|
+
assertLive(node.root);
|
|
1823
|
+
const t = node.target;
|
|
1824
|
+
const snap = t.slice();
|
|
1825
|
+
let result;
|
|
1826
|
+
try {
|
|
1827
|
+
result = Array.prototype[name].apply(t, args);
|
|
1828
|
+
} catch (e) {
|
|
1829
|
+
restoreArray(t, snap);
|
|
1830
|
+
throw e;
|
|
1831
|
+
}
|
|
1832
|
+
finishList(node, snap);
|
|
1833
|
+
return result;
|
|
1834
|
+
};
|
|
1835
|
+
node.methods.set(name, fn);
|
|
1836
|
+
}
|
|
1837
|
+
return fn;
|
|
1838
|
+
}
|
|
1839
|
+
function listHandler(node) {
|
|
1840
|
+
return {
|
|
1841
|
+
get(target, prop, receiver) {
|
|
1842
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
1843
|
+
if (MUTATORS.has(prop)) return listMethod(node, prop);
|
|
1844
|
+
const v = target[prop];
|
|
1845
|
+
if (v !== null && typeof v === "object" && isIndexProp(prop)) {
|
|
1846
|
+
const item = node.desc.item;
|
|
1847
|
+
if (item.kind === "struct") return listElemProxy(node, prop, v);
|
|
1848
|
+
if (item.kind === "list") return nestedListProxy(node, prop, v);
|
|
1849
|
+
}
|
|
1850
|
+
return v;
|
|
1851
|
+
},
|
|
1852
|
+
set(target, prop, value) {
|
|
1853
|
+
if (typeof prop === "string" && (prop === "length" || isIndexProp(prop))) {
|
|
1854
|
+
assertLive(node.root);
|
|
1855
|
+
const snap = target.slice();
|
|
1856
|
+
try {
|
|
1857
|
+
target[prop] = value;
|
|
1858
|
+
} catch (e) {
|
|
1859
|
+
restoreArray(target, snap);
|
|
1860
|
+
throw e;
|
|
1861
|
+
}
|
|
1862
|
+
finishList(node, snap);
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1865
|
+
throw new Error(`${node.label}: cannot set ${String(prop)}`);
|
|
1866
|
+
},
|
|
1867
|
+
deleteProperty(_target, prop) {
|
|
1868
|
+
throw new Error(`${node.label}: cannot delete ${String(prop)}; use splice()`);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function makeListProxy(node) {
|
|
1873
|
+
const p = new Proxy(node.target, listHandler(node));
|
|
1874
|
+
node.root.ctx.proxies.set(node.target, p);
|
|
1875
|
+
return p;
|
|
1876
|
+
}
|
|
1877
|
+
function listProxy(parent, f, value) {
|
|
1878
|
+
const cached = parent.root.ctx.proxies.get(value);
|
|
1879
|
+
if (cached) return cached;
|
|
1880
|
+
return makeListProxy({
|
|
1881
|
+
root: parent.root,
|
|
1882
|
+
target: value,
|
|
1883
|
+
desc: f.type,
|
|
1884
|
+
path: childPath(parent.path, parent.whole, f.index),
|
|
1885
|
+
label: `${parent.label}.${f.name}`,
|
|
1886
|
+
methods: /* @__PURE__ */ new Map()
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
function nestedListProxy(parent, prop, value) {
|
|
1890
|
+
const cached = parent.root.ctx.proxies.get(value);
|
|
1891
|
+
if (cached) return cached;
|
|
1892
|
+
return makeListProxy({
|
|
1893
|
+
root: parent.root,
|
|
1894
|
+
target: value,
|
|
1895
|
+
desc: parent.desc.item,
|
|
1896
|
+
path: parent.path,
|
|
1897
|
+
label: `${parent.label}[${prop}]`,
|
|
1898
|
+
methods: /* @__PURE__ */ new Map()
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
function listElemProxy(parent, prop, value) {
|
|
1902
|
+
const cached = parent.root.ctx.proxies.get(value);
|
|
1903
|
+
if (cached) return cached;
|
|
1904
|
+
return makeObjProxy({
|
|
1905
|
+
root: parent.root,
|
|
1906
|
+
target: value,
|
|
1907
|
+
fields: structFields(parent.desc.item),
|
|
1908
|
+
path: parent.path,
|
|
1909
|
+
whole: true,
|
|
1910
|
+
label: `${parent.label}[${prop}]`
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
var TrackedCollection = class {
|
|
1914
|
+
constructor(ctx, desc, inner) {
|
|
1915
|
+
this.ctx = ctx;
|
|
1916
|
+
this.desc = desc;
|
|
1917
|
+
this.inner = inner;
|
|
1918
|
+
}
|
|
1919
|
+
ctx;
|
|
1920
|
+
desc;
|
|
1921
|
+
inner;
|
|
1922
|
+
roots = /* @__PURE__ */ new Map();
|
|
1923
|
+
invalidate(id) {
|
|
1924
|
+
const root = this.roots.get(id);
|
|
1925
|
+
if (root) {
|
|
1926
|
+
root.live = false;
|
|
1927
|
+
this.roots.delete(id);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
proxyFor(id, value) {
|
|
1931
|
+
const cached = this.ctx.proxies.get(value);
|
|
1932
|
+
if (cached) return cached;
|
|
1933
|
+
let root = this.roots.get(id);
|
|
1934
|
+
if (!root || root.target !== value) {
|
|
1935
|
+
if (root) root.live = false;
|
|
1936
|
+
root = { ctx: this.ctx, collection: this.desc.name, id, target: value, live: true };
|
|
1937
|
+
this.roots.set(id, root);
|
|
1938
|
+
}
|
|
1939
|
+
return makeObjProxy({
|
|
1940
|
+
root,
|
|
1941
|
+
target: value,
|
|
1942
|
+
fields: collectionFields(this.desc),
|
|
1943
|
+
path: [],
|
|
1944
|
+
whole: false,
|
|
1945
|
+
label: `${this.desc.name}[${id}]`
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
add(id, values, options) {
|
|
1949
|
+
this.invalidate(id);
|
|
1950
|
+
const value = this.inner.add(id, values, options);
|
|
1951
|
+
markAdd(this.ctx.dirty, this.desc.name, id);
|
|
1952
|
+
return this.proxyFor(id, value);
|
|
1953
|
+
}
|
|
1954
|
+
remove(id) {
|
|
1955
|
+
const had = this.inner.remove(id);
|
|
1956
|
+
if (had) {
|
|
1957
|
+
this.invalidate(id);
|
|
1958
|
+
markRemove(this.ctx.dirty, this.desc.name, id);
|
|
1959
|
+
}
|
|
1960
|
+
return had;
|
|
1961
|
+
}
|
|
1962
|
+
get(id) {
|
|
1963
|
+
const v = this.inner.get(id);
|
|
1964
|
+
return v === void 0 ? void 0 : this.proxyFor(id, v);
|
|
1965
|
+
}
|
|
1966
|
+
has(id) {
|
|
1967
|
+
return this.inner.has(id);
|
|
1968
|
+
}
|
|
1969
|
+
setOwner(id, owner) {
|
|
1970
|
+
this.inner.setOwner(id, owner);
|
|
1971
|
+
markOwner(this.ctx.dirty, this.desc.name, id);
|
|
1972
|
+
}
|
|
1973
|
+
ownerOf(id) {
|
|
1974
|
+
return this.inner.ownerOf(id);
|
|
1975
|
+
}
|
|
1976
|
+
get size() {
|
|
1977
|
+
return this.inner.size;
|
|
1978
|
+
}
|
|
1979
|
+
ids() {
|
|
1980
|
+
return this.inner.ids();
|
|
1981
|
+
}
|
|
1982
|
+
*[Symbol.iterator]() {
|
|
1983
|
+
for (const [id, value] of this.inner) {
|
|
1984
|
+
yield [id, this.proxyFor(id, value)];
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
function track(schema, state) {
|
|
1989
|
+
const ctx = { dirty: createDirtySet(), proxies: /* @__PURE__ */ new WeakMap() };
|
|
1990
|
+
const descs = /* @__PURE__ */ new Map();
|
|
1991
|
+
for (const c of schema.collections) descs.set(c.name, c);
|
|
1992
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1993
|
+
const proxy = new Proxy(state, {
|
|
1994
|
+
get(target, prop, receiver) {
|
|
1995
|
+
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
1996
|
+
const desc = descs.get(prop);
|
|
1997
|
+
if (!desc) return Reflect.get(target, prop, receiver);
|
|
1998
|
+
let node = cache.get(prop);
|
|
1999
|
+
if (node === void 0) {
|
|
2000
|
+
const raw = target[prop];
|
|
2001
|
+
if (desc.kind === "entity") {
|
|
2002
|
+
if (!(raw instanceof EntityCollection)) {
|
|
2003
|
+
throw new Error(`track: state.${prop} must be an EntityCollection`);
|
|
2004
|
+
}
|
|
2005
|
+
node = new TrackedCollection(ctx, desc, raw);
|
|
2006
|
+
} else {
|
|
2007
|
+
if (raw === null || typeof raw !== "object") {
|
|
2008
|
+
throw new Error(`track: state.${prop} must be an object`);
|
|
2009
|
+
}
|
|
2010
|
+
const value = raw;
|
|
2011
|
+
node = makeObjProxy({
|
|
2012
|
+
root: {
|
|
2013
|
+
ctx,
|
|
2014
|
+
collection: desc.name,
|
|
2015
|
+
id: SINGLETON_ID,
|
|
2016
|
+
target: value,
|
|
2017
|
+
live: true
|
|
2018
|
+
},
|
|
2019
|
+
target: value,
|
|
2020
|
+
fields: collectionFields(desc),
|
|
2021
|
+
path: [],
|
|
2022
|
+
whole: false,
|
|
2023
|
+
label: desc.name
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
cache.set(prop, node);
|
|
2027
|
+
}
|
|
2028
|
+
return node;
|
|
2029
|
+
},
|
|
2030
|
+
set(target, prop, value) {
|
|
2031
|
+
if (typeof prop !== "string") throw new Error(`track: cannot set ${String(prop)}`);
|
|
2032
|
+
const desc = descs.get(prop);
|
|
2033
|
+
if (!desc) throw new Error(`track: unknown collection ${JSON.stringify(prop)}`);
|
|
2034
|
+
if (desc.kind === "entity") {
|
|
2035
|
+
throw new Error(`track: cannot replace entity collection ${JSON.stringify(prop)}`);
|
|
2036
|
+
}
|
|
2037
|
+
const next = normalizeRecord(desc, value);
|
|
2038
|
+
const current = target[prop];
|
|
2039
|
+
for (const f of desc.fields) {
|
|
2040
|
+
current[f.name] = next[f.name];
|
|
2041
|
+
markField(ctx.dirty, desc.name, SINGLETON_ID, [f.index]);
|
|
2042
|
+
}
|
|
2043
|
+
return true;
|
|
2044
|
+
},
|
|
2045
|
+
deleteProperty(_target, prop) {
|
|
2046
|
+
throw new Error(`track: cannot delete ${String(prop)}`);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
return {
|
|
2050
|
+
state: proxy,
|
|
2051
|
+
get dirty() {
|
|
2052
|
+
return ctx.dirty;
|
|
2053
|
+
},
|
|
2054
|
+
flush() {
|
|
2055
|
+
const out = ctx.dirty;
|
|
2056
|
+
ctx.dirty = createDirtySet();
|
|
2057
|
+
return out;
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
function frozenProxy(value, ownershipHint) {
|
|
2062
|
+
if (value === null || typeof value !== "object") return value;
|
|
2063
|
+
return freeze(
|
|
2064
|
+
value,
|
|
2065
|
+
ownershipHint,
|
|
2066
|
+
"",
|
|
2067
|
+
/* @__PURE__ */ new Set(),
|
|
2068
|
+
/* @__PURE__ */ new WeakMap()
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
function freeze(target, hint, prefix, warned, cache) {
|
|
2072
|
+
const cached = cache.get(target);
|
|
2073
|
+
if (cached) return cached;
|
|
2074
|
+
const p = new Proxy(target, {
|
|
2075
|
+
get(t, prop, receiver) {
|
|
2076
|
+
const v = Reflect.get(t, prop, receiver);
|
|
2077
|
+
if (v !== null && typeof v === "object" && typeof prop === "string") {
|
|
2078
|
+
return freeze(v, hint, prefix ? `${prefix}.${prop}` : prop, warned, cache);
|
|
2079
|
+
}
|
|
2080
|
+
return v;
|
|
2081
|
+
},
|
|
2082
|
+
set(_t, prop) {
|
|
2083
|
+
warnFrozen(hint, prefix, String(prop), warned);
|
|
2084
|
+
return true;
|
|
2085
|
+
},
|
|
2086
|
+
deleteProperty(_t, prop) {
|
|
2087
|
+
warnFrozen(hint, prefix, String(prop), warned);
|
|
2088
|
+
return true;
|
|
2089
|
+
},
|
|
2090
|
+
defineProperty(_t, prop) {
|
|
2091
|
+
warnFrozen(hint, prefix, String(prop), warned);
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
cache.set(target, p);
|
|
2096
|
+
return p;
|
|
2097
|
+
}
|
|
2098
|
+
function warnFrozen(hint, prefix, prop, warned) {
|
|
2099
|
+
const path = prefix ? `${prefix}.${prop}` : prop;
|
|
2100
|
+
if (warned.has(path)) return;
|
|
2101
|
+
warned.add(path);
|
|
2102
|
+
console.warn(`irtio: ignoring write to ${path} \u2014 ${hint}`);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/diff.ts
|
|
2106
|
+
function diffSchemas(oldSchema, newSchema) {
|
|
2107
|
+
const changes = [];
|
|
2108
|
+
diffCollections(
|
|
2109
|
+
oldSchema.collections,
|
|
2110
|
+
newSchema.collections,
|
|
2111
|
+
changes
|
|
2112
|
+
);
|
|
2113
|
+
diffRpcs(oldSchema.rpcs, newSchema.rpcs, changes);
|
|
2114
|
+
diffRoles(oldSchema.roles, newSchema.roles, changes);
|
|
2115
|
+
changes.sort(
|
|
2116
|
+
(a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
|
|
2117
|
+
);
|
|
2118
|
+
return changes;
|
|
2119
|
+
}
|
|
2120
|
+
function diffCollections(oldCollections, newCollections, changes) {
|
|
2121
|
+
const oldByName = new Map(oldCollections.map((c) => [c.name, c]));
|
|
2122
|
+
const newByName = new Map(newCollections.map((c) => [c.name, c]));
|
|
2123
|
+
for (const c of oldCollections) {
|
|
2124
|
+
if (!newByName.has(c.name)) {
|
|
2125
|
+
changes.push({
|
|
2126
|
+
kind: "breaking",
|
|
2127
|
+
code: `${c.kind}.removed`,
|
|
2128
|
+
path: c.name,
|
|
2129
|
+
message: `${c.name}: ${c.kind} was removed \u2014 clients/servers referencing it will break`
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
for (const c of newCollections) {
|
|
2134
|
+
if (!oldByName.has(c.name)) {
|
|
2135
|
+
changes.push({
|
|
2136
|
+
kind: "additive",
|
|
2137
|
+
code: `${c.kind}.added`,
|
|
2138
|
+
path: c.name,
|
|
2139
|
+
message: `${c.name}: new ${c.kind} added`
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
for (const oldC of oldCollections) {
|
|
2144
|
+
const newC = newByName.get(oldC.name);
|
|
2145
|
+
if (!newC) continue;
|
|
2146
|
+
if (oldC.kind !== newC.kind) {
|
|
2147
|
+
changes.push({
|
|
2148
|
+
kind: "breaking",
|
|
2149
|
+
code: "entity.kind_changed",
|
|
2150
|
+
path: oldC.name,
|
|
2151
|
+
message: `${oldC.name}: changed from ${oldC.kind} to ${newC.kind} \u2014 this is a breaking shape change`
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
if (oldC.serverOwned !== newC.serverOwned) {
|
|
2155
|
+
changes.push({
|
|
2156
|
+
kind: "breaking",
|
|
2157
|
+
code: "entity.server_owned_changed",
|
|
2158
|
+
path: oldC.name,
|
|
2159
|
+
message: `${oldC.name}: serverOwned changed from ${oldC.serverOwned} to ${newC.serverOwned}`
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (oldC.visibility !== newC.visibility) {
|
|
2163
|
+
changes.push({
|
|
2164
|
+
kind: "breaking",
|
|
2165
|
+
code: "entity.visibility_changed",
|
|
2166
|
+
path: oldC.name,
|
|
2167
|
+
message: `${oldC.name}: visibility changed from ${JSON.stringify(oldC.visibility)} to ${JSON.stringify(newC.visibility)}`
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
if (!sameStringSet(oldC.roles ?? [], newC.roles ?? [])) {
|
|
2171
|
+
changes.push({
|
|
2172
|
+
kind: "breaking",
|
|
2173
|
+
code: "entity.roles_changed",
|
|
2174
|
+
path: oldC.name,
|
|
2175
|
+
message: `${oldC.name}: visible roles changed from [${(oldC.roles ?? []).join(", ")}] to [${(newC.roles ?? []).join(", ")}] \u2014 changes who sees this collection`
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
diffFields(oldC.fields, newC.fields, oldC.name, changes);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
function diffRpcs(oldRpcs, newRpcs, changes) {
|
|
2182
|
+
const oldByName = new Map(oldRpcs.map((r) => [r.name, r]));
|
|
2183
|
+
const newByName = new Map(newRpcs.map((r) => [r.name, r]));
|
|
2184
|
+
for (const r of oldRpcs) {
|
|
2185
|
+
if (!newByName.has(r.name)) {
|
|
2186
|
+
changes.push({
|
|
2187
|
+
kind: "breaking",
|
|
2188
|
+
code: "rpc.removed",
|
|
2189
|
+
path: `rpc.${r.name}`,
|
|
2190
|
+
message: `rpc ${r.name}: was removed`
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
for (const r of newRpcs) {
|
|
2195
|
+
if (!oldByName.has(r.name)) {
|
|
2196
|
+
changes.push({
|
|
2197
|
+
kind: "additive",
|
|
2198
|
+
code: "rpc.added",
|
|
2199
|
+
path: `rpc.${r.name}`,
|
|
2200
|
+
message: `rpc ${r.name}: new ${r.direction} RPC added`
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
for (const oldR of oldRpcs) {
|
|
2205
|
+
const newR = newByName.get(oldR.name);
|
|
2206
|
+
if (!newR) continue;
|
|
2207
|
+
if (oldR.direction !== newR.direction) {
|
|
2208
|
+
changes.push({
|
|
2209
|
+
kind: "breaking",
|
|
2210
|
+
code: "rpc.direction_changed",
|
|
2211
|
+
path: `rpc.${oldR.name}`,
|
|
2212
|
+
message: `rpc ${oldR.name}: direction changed from ${oldR.direction} to ${newR.direction}`
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
diffFields(oldR.params, newR.params, `rpc.${oldR.name}.params`, changes);
|
|
2216
|
+
diffFields(oldR.returns ?? [], newR.returns ?? [], `rpc.${oldR.name}.returns`, changes);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function diffRoles(oldRoles, newRoles, changes) {
|
|
2220
|
+
const oldSet = new Set(oldRoles);
|
|
2221
|
+
const newSet = new Set(newRoles);
|
|
2222
|
+
for (const r of oldRoles) {
|
|
2223
|
+
if (!newSet.has(r)) {
|
|
2224
|
+
changes.push({
|
|
2225
|
+
kind: "breaking",
|
|
2226
|
+
code: "role.removed",
|
|
2227
|
+
path: "roles",
|
|
2228
|
+
message: `roles: role ${JSON.stringify(r)} was removed`
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
for (const r of newRoles) {
|
|
2233
|
+
if (!oldSet.has(r)) {
|
|
2234
|
+
changes.push({
|
|
2235
|
+
kind: "additive",
|
|
2236
|
+
code: "role.added",
|
|
2237
|
+
path: "roles",
|
|
2238
|
+
message: `roles: new role ${JSON.stringify(r)} added`
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function diffFields(oldFields, newFields, basePath, changes) {
|
|
2244
|
+
const oldByName = new Map(oldFields.map((f) => [f.name, f]));
|
|
2245
|
+
const newByName = new Map(newFields.map((f) => [f.name, f]));
|
|
2246
|
+
const removedNames = oldFields.filter((f) => !newByName.has(f.name)).map((f) => f.name);
|
|
2247
|
+
const addedNames = newFields.filter((f) => !oldByName.has(f.name)).map((f) => f.name);
|
|
2248
|
+
for (const name of removedNames) {
|
|
2249
|
+
changes.push({
|
|
2250
|
+
kind: "breaking",
|
|
2251
|
+
code: "field.removed",
|
|
2252
|
+
path: `${basePath}.${name}`,
|
|
2253
|
+
message: `${basePath}.${name}: field was removed (breaking)`
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
for (const name of addedNames) {
|
|
2257
|
+
const f = newByName.get(name);
|
|
2258
|
+
const additive = f.type.hasDefault || f.type.opt;
|
|
2259
|
+
changes.push({
|
|
2260
|
+
kind: additive ? "additive" : "breaking",
|
|
2261
|
+
code: "field.added",
|
|
2262
|
+
path: `${basePath}.${name}`,
|
|
2263
|
+
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`
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
if (removedNames.length === 1 && addedNames.length === 1) {
|
|
2267
|
+
const removedName = removedNames[0];
|
|
2268
|
+
const addedName = addedNames[0];
|
|
2269
|
+
const oldF = oldByName.get(removedName);
|
|
2270
|
+
const newF = newByName.get(addedName);
|
|
2271
|
+
if (oldF.index === newF.index && typeSignature(oldF.type) === typeSignature(newF.type)) {
|
|
2272
|
+
changes.push({
|
|
2273
|
+
kind: "breaking",
|
|
2274
|
+
code: "field.renamed",
|
|
2275
|
+
path: `${basePath}.${addedName}`,
|
|
2276
|
+
message: `${basePath}.${removedName} -> ${basePath}.${addedName}: looks like a rename; write a transform`
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
for (const oldF of oldFields) {
|
|
2281
|
+
const newF = newByName.get(oldF.name);
|
|
2282
|
+
if (!newF) continue;
|
|
2283
|
+
const path = `${basePath}.${oldF.name}`;
|
|
2284
|
+
if (oldF.index !== newF.index) {
|
|
2285
|
+
changes.push({
|
|
2286
|
+
kind: "breaking",
|
|
2287
|
+
code: "field.reordered",
|
|
2288
|
+
path,
|
|
2289
|
+
message: `${path}: declaration order changed (wire index ${oldF.index} -> ${newF.index}) \u2014 breaking`
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
diffType(oldF.type, newF.type, path, changes);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function diffType(oldT, newT, path, changes) {
|
|
2296
|
+
if (oldT.kind !== newT.kind) {
|
|
2297
|
+
changes.push({
|
|
2298
|
+
kind: "breaking",
|
|
2299
|
+
code: "field.type_changed",
|
|
2300
|
+
path,
|
|
2301
|
+
message: `${path}: type changed from ${describeType(oldT)} to ${describeType(newT)} (breaking)`
|
|
2302
|
+
});
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (!oldT.opt && newT.opt) {
|
|
2306
|
+
changes.push({
|
|
2307
|
+
kind: "additive",
|
|
2308
|
+
code: "field.opt_added",
|
|
2309
|
+
path,
|
|
2310
|
+
message: `${path}: became .opt (required -> optional), safe for existing clients`
|
|
2311
|
+
});
|
|
2312
|
+
} else if (oldT.opt && !newT.opt) {
|
|
2313
|
+
changes.push({
|
|
2314
|
+
kind: "breaking",
|
|
2315
|
+
code: "field.opt_removed",
|
|
2316
|
+
path,
|
|
2317
|
+
message: `${path}: no longer .opt (optional -> required) \u2014 breaking`
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
diffDefault(oldT, newT, path, changes);
|
|
2321
|
+
switch (oldT.kind) {
|
|
2322
|
+
case "str":
|
|
2323
|
+
diffMax(oldT.max, newT.max, path, changes);
|
|
2324
|
+
break;
|
|
2325
|
+
case "ref": {
|
|
2326
|
+
const newRef = newT;
|
|
2327
|
+
if (oldT.entity !== newRef.entity) {
|
|
2328
|
+
changes.push({
|
|
2329
|
+
kind: "breaking",
|
|
2330
|
+
code: "field.ref_target_changed",
|
|
2331
|
+
path,
|
|
2332
|
+
message: `${path}: ref target changed from ${JSON.stringify(oldT.entity)} to ${JSON.stringify(newRef.entity)} (breaking)`
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
case "enum":
|
|
2338
|
+
diffEnum(oldT.values, newT.values, path, changes);
|
|
2339
|
+
break;
|
|
2340
|
+
case "list": {
|
|
2341
|
+
const newList = newT;
|
|
2342
|
+
diffMax(oldT.max, newList.max, path, changes);
|
|
2343
|
+
diffType(oldT.item, newList.item, `${path}[]`, changes);
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
case "struct": {
|
|
2347
|
+
const newStruct = newT;
|
|
2348
|
+
diffFields(structFieldDescs(oldT.fields), structFieldDescs(newStruct.fields), path, changes);
|
|
2349
|
+
break;
|
|
2350
|
+
}
|
|
2351
|
+
default:
|
|
2352
|
+
break;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
function diffMax(oldMax, newMax, path, changes) {
|
|
2356
|
+
if (newMax > oldMax) {
|
|
2357
|
+
changes.push({
|
|
2358
|
+
kind: "additive",
|
|
2359
|
+
code: "field.max_increased",
|
|
2360
|
+
path,
|
|
2361
|
+
message: `${path}: max increased from ${oldMax} to ${newMax}, safe for existing values`
|
|
2362
|
+
});
|
|
2363
|
+
} else if (newMax < oldMax) {
|
|
2364
|
+
changes.push({
|
|
2365
|
+
kind: "breaking",
|
|
2366
|
+
code: "field.max_decreased",
|
|
2367
|
+
path,
|
|
2368
|
+
message: `${path}: max decreased from ${oldMax} to ${newMax} \u2014 existing values may no longer fit (breaking)`
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
function diffEnum(oldValues, newValues, path, changes) {
|
|
2373
|
+
if (arraysEqual(oldValues, newValues)) return;
|
|
2374
|
+
const isPureAppend = oldValues.length < newValues.length && oldValues.every((v, i) => newValues[i] === v);
|
|
2375
|
+
if (isPureAppend) {
|
|
2376
|
+
changes.push({
|
|
2377
|
+
kind: "additive",
|
|
2378
|
+
code: "enum.appended",
|
|
2379
|
+
path,
|
|
2380
|
+
message: `${path}: enum values appended (${newValues.slice(oldValues.length).join(", ")}), old values unchanged`
|
|
2381
|
+
});
|
|
2382
|
+
} else {
|
|
2383
|
+
changes.push({
|
|
2384
|
+
kind: "breaking",
|
|
2385
|
+
code: "enum.reordered",
|
|
2386
|
+
path,
|
|
2387
|
+
message: `${path}: enum values changed from [${oldValues.join(", ")}] to [${newValues.join(", ")}] \u2014 reorder/rename/remove breaks the wire index (breaking)`
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
function diffDefault(oldT, newT, path, changes) {
|
|
2392
|
+
if (!oldT.hasDefault && newT.hasDefault) {
|
|
2393
|
+
changes.push({
|
|
2394
|
+
kind: "additive",
|
|
2395
|
+
code: "field.default_added",
|
|
2396
|
+
path,
|
|
2397
|
+
message: `${path}: gained a .default(...), existing snapshots keep their current values`
|
|
2398
|
+
});
|
|
2399
|
+
} else if (oldT.hasDefault && !newT.hasDefault) {
|
|
2400
|
+
changes.push({
|
|
2401
|
+
kind: "additive",
|
|
2402
|
+
code: "field.default_removed",
|
|
2403
|
+
path,
|
|
2404
|
+
message: `${path}: .default(...) removed; existing snapshots already have values, so this is safe`
|
|
2405
|
+
});
|
|
2406
|
+
} else if (oldT.hasDefault && newT.hasDefault && stableStringify(oldT.default) !== stableStringify(newT.default)) {
|
|
2407
|
+
changes.push({
|
|
2408
|
+
kind: "additive",
|
|
2409
|
+
code: "field.default_changed",
|
|
2410
|
+
path,
|
|
2411
|
+
message: `${path}: .default(...) value changed; clients with old snapshots keep their existing values`
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function structFieldDescs(fields) {
|
|
2416
|
+
return Object.entries(fields).map(([name, type], index) => ({ name, index, type }));
|
|
2417
|
+
}
|
|
2418
|
+
function sameStringSet(a, b) {
|
|
2419
|
+
if (a.length !== b.length) return false;
|
|
2420
|
+
const setA = new Set(a);
|
|
2421
|
+
for (const v of b) if (!setA.has(v)) return false;
|
|
2422
|
+
return true;
|
|
2423
|
+
}
|
|
2424
|
+
function arraysEqual(a, b) {
|
|
2425
|
+
if (a.length !== b.length) return false;
|
|
2426
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2427
|
+
return true;
|
|
2428
|
+
}
|
|
2429
|
+
function typeSignature(d) {
|
|
2430
|
+
const o = { kind: d.kind, opt: d.opt };
|
|
2431
|
+
switch (d.kind) {
|
|
2432
|
+
case "str":
|
|
2433
|
+
o.max = d.max;
|
|
2434
|
+
break;
|
|
2435
|
+
case "enum":
|
|
2436
|
+
o.values = [...d.values];
|
|
2437
|
+
break;
|
|
2438
|
+
case "ref":
|
|
2439
|
+
o.entity = d.entity;
|
|
2440
|
+
break;
|
|
2441
|
+
case "list":
|
|
2442
|
+
o.max = d.max;
|
|
2443
|
+
o.item = typeSignature(d.item);
|
|
2444
|
+
break;
|
|
2445
|
+
case "struct":
|
|
2446
|
+
o.fields = Object.entries(d.fields).map(([k, v]) => [k, typeSignature(v)]);
|
|
2447
|
+
break;
|
|
2448
|
+
default:
|
|
2449
|
+
break;
|
|
2450
|
+
}
|
|
2451
|
+
return stableStringify(o);
|
|
2452
|
+
}
|
|
2453
|
+
export {
|
|
2454
|
+
ByteReader,
|
|
2455
|
+
ByteWriter,
|
|
2456
|
+
CANONICAL_VERSION,
|
|
2457
|
+
EntityCollection,
|
|
2458
|
+
ID_MAX_BYTES,
|
|
2459
|
+
RESERVED_COLLECTION_NAMES,
|
|
2460
|
+
RESERVED_RPC_NAMES,
|
|
2461
|
+
SERVER_OWNER,
|
|
2462
|
+
SINGLETON_ID,
|
|
2463
|
+
applyDelta,
|
|
2464
|
+
bool,
|
|
2465
|
+
bytesEqual,
|
|
2466
|
+
canonicalType,
|
|
2467
|
+
client,
|
|
2468
|
+
cloneValue,
|
|
2469
|
+
collectionDirty,
|
|
2470
|
+
computeDirty,
|
|
2471
|
+
createDirtySet,
|
|
2472
|
+
createFieldMask,
|
|
2473
|
+
createState,
|
|
2474
|
+
decodeDelta,
|
|
2475
|
+
decodeDeltaFrom,
|
|
2476
|
+
decodeFields,
|
|
2477
|
+
decodeSnapshot,
|
|
2478
|
+
defaultRecord,
|
|
2479
|
+
defaultValue,
|
|
2480
|
+
defineSchema,
|
|
2481
|
+
describeType,
|
|
2482
|
+
diffSchemas,
|
|
2483
|
+
encodeDelta,
|
|
2484
|
+
encodeFields,
|
|
2485
|
+
encodeSnapshot,
|
|
2486
|
+
entity,
|
|
2487
|
+
enumOf,
|
|
2488
|
+
estimateSize,
|
|
2489
|
+
f32,
|
|
2490
|
+
f64,
|
|
2491
|
+
filterDirty,
|
|
2492
|
+
frozenProxy,
|
|
2493
|
+
i32,
|
|
2494
|
+
isDirtyEmpty,
|
|
2495
|
+
isWhole,
|
|
2496
|
+
list,
|
|
2497
|
+
markAdd,
|
|
2498
|
+
markField,
|
|
2499
|
+
markOwner,
|
|
2500
|
+
markPath,
|
|
2501
|
+
markRemove,
|
|
2502
|
+
mergeDirty,
|
|
2503
|
+
mergeMask,
|
|
2504
|
+
normalizeRecord,
|
|
2505
|
+
normalizeValue,
|
|
2506
|
+
readValue,
|
|
2507
|
+
ref,
|
|
2508
|
+
schemaFromCanonical,
|
|
2509
|
+
server,
|
|
2510
|
+
sha256,
|
|
2511
|
+
sha256Hex,
|
|
2512
|
+
singleton,
|
|
2513
|
+
stableStringify,
|
|
2514
|
+
str,
|
|
2515
|
+
struct,
|
|
2516
|
+
toHex,
|
|
2517
|
+
track,
|
|
2518
|
+
u16,
|
|
2519
|
+
u32,
|
|
2520
|
+
u8,
|
|
2521
|
+
validateForDeploy,
|
|
2522
|
+
validateValue,
|
|
2523
|
+
writeValue
|
|
2524
|
+
};
|