@irtio/schema 0.5.2 → 0.6.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/dist/index.d.ts CHANGED
@@ -159,6 +159,46 @@ declare function compilePhysics(at: string, kind: 'entity' | 'singleton', fields
159
159
  declare function canonicalPhysics(p: PhysicsDesc): Record<string, unknown>;
160
160
  /** Reads a canonical physics fragment back (`schemaFromCanonical`). */
161
161
  declare function physicsFromCanonical(at: string, raw: unknown): EntityPhysics;
162
+ /**
163
+ * One planar body's state, in the vocabulary a 2D engine speaks. This is the only place the
164
+ * mapping from two dimensions onto the thirteen 3D channels is written down, and it lives in the
165
+ * schema package precisely because two sides have to agree about it: the room's server-side world
166
+ * writes through it, and anything reading those channels back (a renderer deriving the angle, a
167
+ * client-side world) reverses it. A second copy of these six lines somewhere else would be a
168
+ * silent way for the two to disagree by a sign.
169
+ *
170
+ * `angle` rides the quaternion about Z, which is what a rotation in the xy plane *is*:
171
+ * `qz = sin(a/2)`, `qw = cos(a/2)`, and back out with `2 * atan2(qz, qw)`. Channels a plane has no
172
+ * use for (`z`, `qx`, `qy`, `vz`, `wx`, `wy`) read as constant zero rather than being omitted, so a
173
+ * schema written for one engine still decodes under the other.
174
+ */
175
+ interface Body2dState {
176
+ readonly x: number;
177
+ readonly y: number;
178
+ /** Radians, counter-clockwise. */
179
+ readonly angle: number;
180
+ readonly vx: number;
181
+ readonly vy: number;
182
+ /** Radians per second about Z. */
183
+ readonly angularVelocity: number;
184
+ }
185
+ /** The value a planar body puts on one 3D channel. */
186
+ declare function channelOf2d(channel: PhysicsBodyChannel, b: Body2dState): number;
187
+ /** Reads the angle back out of the two quaternion channels a plane uses. */
188
+ declare function angleFrom2d(qz: number, qw: number): number;
189
+ /**
190
+ * The reverse of {@link channelOf2d}: folds one channel's value into a mutable planar state.
191
+ * `qz`/`qw` are accumulated and the angle recomputed, so either order of arrival works.
192
+ */
193
+ declare function applyChannel2d(channel: PhysicsBodyChannel, value: number, target: {
194
+ x: number;
195
+ y: number;
196
+ qz: number;
197
+ qw: number;
198
+ vx: number;
199
+ vy: number;
200
+ wz: number;
201
+ }): void;
162
202
 
163
203
  /**
164
204
  * `entity`, `singleton`, `server`, `client`, `defineSchema`, and the compiled schema
@@ -774,6 +814,59 @@ declare function applyDelta(schema: AnySchema, state: PlainState, delta: Delta):
774
814
  /** Field-level diff between two plain states: what would have to be sent to turn `from` into `to`. */
775
815
  declare function computeDirty(schema: AnySchema, from: PlainState, to: PlainState): DirtySet;
776
816
 
817
+ /**
818
+ * The measurement walker: a reader's-eye pass over *encoded* delta and snapshot bytes that
819
+ * reports where each byte went, keyed by collection and field.
820
+ *
821
+ * This is deliberately a second reader of the wire format rather than an instrumented writer.
822
+ * The encoders and `ValueSink` are untouched, so the hot path stays byte-identical and
823
+ * cost-identical whether or not anything is profiling; and because the walker reads exactly what
824
+ * `decodeDeltaFrom`/`decodeSnapshot` read, it works on the server after an encode and on the
825
+ * client before a decode, from the same code.
826
+ *
827
+ * The copy is kept honest by conservation: the spans it reports partition the bytes it consumed,
828
+ * with nothing counted twice and nothing left over (`walk.test.ts` proves it against
829
+ * `encodeDelta(...).length` over random schemas). If the format changes here without changing
830
+ * `codec.ts`, or the other way round, that property test fails.
831
+ *
832
+ * Nothing here allocates a decoded value: it skips values by width, so a profiled room pays for
833
+ * a second pass over the bytes and not for a second decode.
834
+ */
835
+
836
+ /**
837
+ * Where the bytes went. Every callback reports a byte count, and for one walk the counts sum to
838
+ * the number of bytes the walk consumed.
839
+ */
840
+ interface WalkEvents {
841
+ /**
842
+ * Framing that belongs to no single field: `tick`, `hash8`, the dirty-collection count, and
843
+ * each collection's index and op count (snapshot: the header and each entity count).
844
+ */
845
+ header(bytes: number): void;
846
+ /**
847
+ * One op's own framing: the tag byte, the id string, the owner string (add), the dirty mask
848
+ * and the owner string when the owner bit is set (update). Always called before that op's
849
+ * fields, so a consumer can route a whole op somewhere else by latching on this call.
850
+ *
851
+ * A snapshot's records report as `add` ops whose framing is the id and owner strings; a
852
+ * snapshot singleton has no op framing at all, only fields.
853
+ */
854
+ op(collection: CollectionDesc, kind: 'add' | 'update' | 'remove', id: string, bytes: number): void;
855
+ /** One field value, nested struct masks and presence bytes included. */
856
+ field(collection: CollectionDesc, field: FieldDesc, bytes: number): void;
857
+ }
858
+ /**
859
+ * Walks one delta body, reporting spans through `events`. Accepts raw bytes or a positioned
860
+ * reader, which it leaves at the first byte after the body exactly as `decodeDeltaFrom` does
861
+ * (the `CORRECT` frame's tick suffix is the caller's). Returns the number of bytes consumed.
862
+ */
863
+ declare function walkDelta(schema: AnySchema, bytes: Uint8Array | ByteReader, events: WalkEvents): number;
864
+ /**
865
+ * Walks one snapshot body, reporting spans through `events`. Entity records report as `add` ops;
866
+ * a singleton reports only its fields. Returns the number of bytes consumed.
867
+ */
868
+ declare function walkSnapshot(schema: AnySchema, bytes: Uint8Array | ByteReader, events: WalkEvents): number;
869
+
777
870
  /**
778
871
  * `track()` — a proxy tree over a plain state that records every write into a `DirtySet`.
779
872
  *
@@ -824,4 +917,4 @@ interface SchemaChange {
824
917
  /** Compares two compiled schemas and returns every classified change, sorted by path then code. */
825
918
  declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
826
919
 
827
- export { type AddOptions, type AnyDef, type AnyRpc, type AnySchema, type AnyType, type BodyFieldsOf, type BroadcastProxy, ByteReader, ByteWriter, CANONICAL_VERSION, type ClientCallProxy, type ClientImplementations, type ClientRpcs, type ClientState, type Collection, type CollectionDesc, type CollectionDirty, type DecodedSnapshot, type DeepReadonly, type DeepWritable, type DefMap, type Delta, type DeltaCollection, type DeltaOp, type DirtySet, EntityCollection, type EntityDef, type EntityOptions, type EntityPhysics, type FieldDesc, type FieldMask, type Fields, type FromCanonicalOptions, type GridDesc, type GridOptions, type Header, ID_MAX_BYTES, type Implementations, type Infer, type InferFields, type InitFields, type InitOf, type InstanceOf, type Kind, type OwnableKeys, type Owned, PHYSICS_BODY_CHANNELS, type PhysicsBodyChannel, type PhysicsBodyMap, type PhysicsDesc, type PhysicsKeys, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, type ReadonlyBodyFields, type ReadonlyCollection, type RecordDirty, type RoleOf, type RpcDef, type RpcDesc, type RpcDirection, type RpcMap, type RpcParams, type RpcReturns, type RpcSpec, SERVER_OWNER, SINGLETON_ID, type Schema, type SchemaChange, type SchemaDefs, type SchemaIssue, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerOwnedKeys, type ServerRpcs, type SingletonDef, type SnapshotOptions, type State, type Tracked, type Type, type TypeDesc, type ValueSink, type Visibility, type VisibleKeys, applyDelta, bool, bytesEqual, canonicalPhysics, canonicalType, client, cloneValue, collectionDirty, compilePhysics, computeDirty, createDirtySet, createFieldMask, createState, decodeDelta, decodeDeltaFrom, decodeFields, decodeSnapshot, defaultRecord, defaultValue, defineSchema, describeType, diffSchemas, encodeDelta, encodeFields, encodeSnapshot, entity, enumOf, estimateSize, f32, f64, filterDirty, frozenProxy, i32, isDirtyEmpty, isWhole, list, markAdd, markField, markOwner, markPath, markRemove, mergeDirty, mergeMask, normalizeRecord, normalizeValue, physicsFromCanonical, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, writeValue };
920
+ export { type AddOptions, type AnyDef, type AnyRpc, type AnySchema, type AnyType, type Body2dState, type BodyFieldsOf, type BroadcastProxy, ByteReader, ByteWriter, CANONICAL_VERSION, type ClientCallProxy, type ClientImplementations, type ClientRpcs, type ClientState, type Collection, type CollectionDesc, type CollectionDirty, type DecodedSnapshot, type DeepReadonly, type DeepWritable, type DefMap, type Delta, type DeltaCollection, type DeltaOp, type DirtySet, EntityCollection, type EntityDef, type EntityOptions, type EntityPhysics, type FieldDesc, type FieldMask, type Fields, type FromCanonicalOptions, type GridDesc, type GridOptions, type Header, ID_MAX_BYTES, type Implementations, type Infer, type InferFields, type InitFields, type InitOf, type InstanceOf, type Kind, type OwnableKeys, type Owned, PHYSICS_BODY_CHANNELS, type PhysicsBodyChannel, type PhysicsBodyMap, type PhysicsDesc, type PhysicsKeys, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, type ReadonlyBodyFields, type ReadonlyCollection, type RecordDirty, type RoleOf, type RpcDef, type RpcDesc, type RpcDirection, type RpcMap, type RpcParams, type RpcReturns, type RpcSpec, SERVER_OWNER, SINGLETON_ID, type Schema, type SchemaChange, type SchemaDefs, type SchemaIssue, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerOwnedKeys, type ServerRpcs, type SingletonDef, type SnapshotOptions, type State, type Tracked, type Type, type TypeDesc, type ValueSink, type Visibility, type VisibleKeys, type WalkEvents, angleFrom2d, applyChannel2d, applyDelta, bool, bytesEqual, canonicalPhysics, canonicalType, channelOf2d, client, cloneValue, collectionDirty, compilePhysics, computeDirty, createDirtySet, createFieldMask, createState, decodeDelta, decodeDeltaFrom, decodeFields, decodeSnapshot, defaultRecord, defaultValue, defineSchema, describeType, diffSchemas, encodeDelta, encodeFields, encodeSnapshot, entity, enumOf, estimateSize, f32, f64, filterDirty, frozenProxy, i32, isDirtyEmpty, isWhole, list, markAdd, markField, markOwner, markPath, markRemove, mergeDirty, mergeMask, normalizeRecord, normalizeValue, physicsFromCanonical, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, walkDelta, walkSnapshot, writeValue };
package/dist/index.js CHANGED
@@ -318,6 +318,56 @@ function physicsFromCanonical(at, raw) {
318
318
  }
319
319
  return { body, intents };
320
320
  }
321
+ function channelOf2d(channel, b) {
322
+ switch (channel) {
323
+ case "x":
324
+ return b.x;
325
+ case "y":
326
+ return b.y;
327
+ case "qz":
328
+ return Math.sin(b.angle / 2);
329
+ case "qw":
330
+ return Math.cos(b.angle / 2);
331
+ case "vx":
332
+ return b.vx;
333
+ case "vy":
334
+ return b.vy;
335
+ case "wz":
336
+ return b.angularVelocity;
337
+ default:
338
+ return 0;
339
+ }
340
+ }
341
+ function angleFrom2d(qz, qw) {
342
+ return 2 * Math.atan2(qz, qw);
343
+ }
344
+ function applyChannel2d(channel, value, target) {
345
+ switch (channel) {
346
+ case "x":
347
+ target.x = value;
348
+ return;
349
+ case "y":
350
+ target.y = value;
351
+ return;
352
+ case "qz":
353
+ target.qz = value;
354
+ return;
355
+ case "qw":
356
+ target.qw = value;
357
+ return;
358
+ case "vx":
359
+ target.vx = value;
360
+ return;
361
+ case "vy":
362
+ target.vy = value;
363
+ return;
364
+ case "wz":
365
+ target.wz = value;
366
+ return;
367
+ default:
368
+ return;
369
+ }
370
+ }
321
371
 
322
372
  // src/sha256.ts
323
373
  var K = new Uint32Array([
@@ -1901,6 +1951,147 @@ function computeDirty(schema, from, to) {
1901
1951
  return dirty;
1902
1952
  }
1903
1953
 
1954
+ // src/walk.ts
1955
+ function skip(r, n) {
1956
+ if (n < 0 || r.remaining < n) throw new Error("unexpected end of buffer");
1957
+ r.pos += n;
1958
+ }
1959
+ function structNames2(d) {
1960
+ return Object.keys(d.fields);
1961
+ }
1962
+ function skipValue(r, desc) {
1963
+ if (desc.opt && r.u8() === 0) return;
1964
+ switch (desc.kind) {
1965
+ case "bool":
1966
+ case "u8":
1967
+ case "enum":
1968
+ skip(r, 1);
1969
+ return;
1970
+ case "u16":
1971
+ skip(r, 2);
1972
+ return;
1973
+ case "u32":
1974
+ case "i32":
1975
+ case "f32":
1976
+ skip(r, 4);
1977
+ return;
1978
+ case "f64":
1979
+ skip(r, 8);
1980
+ return;
1981
+ case "str":
1982
+ case "ref":
1983
+ skip(r, r.varint());
1984
+ return;
1985
+ case "list": {
1986
+ const n = r.varint();
1987
+ for (let i = 0; i < n; i++) skipValue(r, desc.item);
1988
+ return;
1989
+ }
1990
+ case "struct": {
1991
+ for (const fd of Object.values(desc.fields)) skipValue(r, fd);
1992
+ return;
1993
+ }
1994
+ }
1995
+ }
1996
+ function readBit2(mask, i) {
1997
+ return ((mask[i >> 3] ?? 0) & 1 << (i & 7)) !== 0;
1998
+ }
1999
+ function skipDirtyField(r, desc) {
2000
+ if (desc.kind !== "struct") {
2001
+ skipValue(r, desc);
2002
+ return;
2003
+ }
2004
+ if (desc.opt && r.u8() === 0) return;
2005
+ const names = structNames2(desc);
2006
+ const maskBytes = r.bytes(names.length + 7 >> 3);
2007
+ for (let i = 0; i < names.length; i++) {
2008
+ if (!readBit2(maskBytes, i)) continue;
2009
+ skipDirtyField(r, desc.fields[names[i]]);
2010
+ }
2011
+ }
2012
+ function readerFor(bytes) {
2013
+ return bytes instanceof ByteReader ? bytes : new ByteReader(bytes);
2014
+ }
2015
+ function walkDelta(schema, bytes, events) {
2016
+ const r = readerFor(bytes);
2017
+ const from = r.pos;
2018
+ let at = r.pos;
2019
+ skip(r, 12);
2020
+ const count = r.varint();
2021
+ events.header(r.pos - at);
2022
+ for (let ci = 0; ci < count; ci++) {
2023
+ at = r.pos;
2024
+ const index = r.varint();
2025
+ const c = schema.collections[index];
2026
+ if (!c) throw new Error(`delta references unknown collection index ${index}`);
2027
+ const opCount = r.varint();
2028
+ events.header(r.pos - at);
2029
+ for (let oi = 0; oi < opCount; oi++) {
2030
+ at = r.pos;
2031
+ const tag = r.u8();
2032
+ const id = r.str();
2033
+ if (tag === 2) {
2034
+ events.op(c, "remove", id, r.pos - at);
2035
+ continue;
2036
+ }
2037
+ if (tag === 0) {
2038
+ skip(r, r.varint());
2039
+ events.op(c, "add", id, r.pos - at);
2040
+ for (const f of c.fields) {
2041
+ const start = r.pos;
2042
+ skipValue(r, f.type);
2043
+ events.field(c, f, r.pos - start);
2044
+ }
2045
+ continue;
2046
+ }
2047
+ if (tag !== 1) throw new Error(`unknown delta op byte ${tag} in ${c.name}`);
2048
+ const n = c.fields.length;
2049
+ const maskBytes = r.bytes(n + 1 + 7 >> 3).slice();
2050
+ if (readBit2(maskBytes, n)) skip(r, r.varint());
2051
+ events.op(c, "update", id, r.pos - at);
2052
+ for (let i = 0; i < n; i++) {
2053
+ if (!readBit2(maskBytes, i)) continue;
2054
+ const f = c.fields[i];
2055
+ const start = r.pos;
2056
+ skipDirtyField(r, f.type);
2057
+ events.field(c, f, r.pos - start);
2058
+ }
2059
+ }
2060
+ }
2061
+ return r.pos - from;
2062
+ }
2063
+ function walkSnapshot(schema, bytes, events) {
2064
+ const r = readerFor(bytes);
2065
+ const from = r.pos;
2066
+ skip(r, 12);
2067
+ events.header(12);
2068
+ for (const c of schema.collections) {
2069
+ if (c.kind === "entity") {
2070
+ let at = r.pos;
2071
+ const n = r.varint();
2072
+ events.header(r.pos - at);
2073
+ for (let i = 0; i < n; i++) {
2074
+ at = r.pos;
2075
+ const id = r.str();
2076
+ skip(r, r.varint());
2077
+ events.op(c, "add", id, r.pos - at);
2078
+ for (const f of c.fields) {
2079
+ const start = r.pos;
2080
+ skipValue(r, f.type);
2081
+ events.field(c, f, r.pos - start);
2082
+ }
2083
+ }
2084
+ } else {
2085
+ for (const f of c.fields) {
2086
+ const start = r.pos;
2087
+ skipValue(r, f.type);
2088
+ events.field(c, f, r.pos - start);
2089
+ }
2090
+ }
2091
+ }
2092
+ return r.pos - from;
2093
+ }
2094
+
1904
2095
  // src/track.ts
1905
2096
  var fieldTables = /* @__PURE__ */ new WeakMap();
1906
2097
  function structFields(d) {
@@ -2388,6 +2579,14 @@ function diffCollections(oldCollections, newCollections, changes) {
2388
2579
  message: `${oldC.name}: visible roles changed from [${(oldC.roles ?? []).join(", ")}] to [${(newC.roles ?? []).join(", ")}] \u2014 changes who sees this collection`
2389
2580
  });
2390
2581
  }
2582
+ if (oldC.index !== newC.index) {
2583
+ changes.push({
2584
+ kind: "breaking",
2585
+ code: `${oldC.kind}.reordered`,
2586
+ path: oldC.name,
2587
+ message: `${oldC.name}: wire index changed (${oldC.index} -> ${newC.index}) \u2014 collections are addressed positionally, so every delta and snapshot for this collection would be misread (breaking)`
2588
+ });
2589
+ }
2391
2590
  if (stableStringify(oldC.grid) !== stableStringify(newC.grid)) {
2392
2591
  changes.push({
2393
2592
  kind: "breaking",
@@ -2422,9 +2621,25 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
2422
2621
  });
2423
2622
  }
2424
2623
  }
2624
+ if (oldRpcs.length !== newRpcs.length) {
2625
+ changes.push({
2626
+ kind: "breaking",
2627
+ code: "rpc.builtin_reordered",
2628
+ path: "rpc",
2629
+ message: `rpc: the number of RPCs changed from ${oldRpcs.length} to ${newRpcs.length}, which shifts the ids of irtio's built-in RPCs (they are numbered after yours) \u2014 connected clients would call the wrong one (breaking)`
2630
+ });
2631
+ }
2425
2632
  for (const oldR of oldRpcs) {
2426
2633
  const newR = newByName.get(oldR.name);
2427
2634
  if (!newR) continue;
2635
+ if (oldR.index !== newR.index) {
2636
+ changes.push({
2637
+ kind: "breaking",
2638
+ code: "rpc.reordered",
2639
+ path: `rpc.${oldR.name}`,
2640
+ message: `rpc ${oldR.name}: wire id changed (${oldR.index} -> ${newR.index}) \u2014 RPCs are addressed by a positional id, so calls would reach the wrong handler (breaking)`
2641
+ });
2642
+ }
2428
2643
  if (oldR.direction !== newR.direction) {
2429
2644
  changes.push({
2430
2645
  kind: "breaking",
@@ -2682,11 +2897,14 @@ export {
2682
2897
  RESERVED_RPC_NAMES,
2683
2898
  SERVER_OWNER,
2684
2899
  SINGLETON_ID,
2900
+ angleFrom2d,
2901
+ applyChannel2d,
2685
2902
  applyDelta,
2686
2903
  bool,
2687
2904
  bytesEqual,
2688
2905
  canonicalPhysics,
2689
2906
  canonicalType,
2907
+ channelOf2d,
2690
2908
  client,
2691
2909
  cloneValue,
2692
2910
  collectionDirty,
@@ -2745,5 +2963,7 @@ export {
2745
2963
  u8,
2746
2964
  validateForDeploy,
2747
2965
  validateValue,
2966
+ walkDelta,
2967
+ walkSnapshot,
2748
2968
  writeValue
2749
2969
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/schema",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "irtio schema DSL, type inference, canonical hash, codec, change tracking, and schema diff",
5
5
  "license": "MIT",
6
6
  "publishConfig": {