@irtio/schema 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -244,6 +244,23 @@ interface EntityOptions {
244
244
  * the schema hash, and `schemaFromCanonical` does not round-trip it. Requires `physics`.
245
245
  */
246
246
  readonly predicted?: boolean;
247
+ /**
248
+ * D71: `false` → instances of this collection the client does not predict get **no body** in the
249
+ * client's local world, which is what every unpredicted body had before proxies existed.
250
+ *
251
+ * The default is `true`. An instance the predictor skips — one past `maxPredictedBodies` in a
252
+ * `predicted` collection, or any instance of a collection that is not `predicted` — is given a
253
+ * **kinematic proxy**: the same collider, built by the same body factory, moved each local step
254
+ * to the pose the renderer draws for it and never simulated forward. Predicted bodies collide
255
+ * with it; local forces never move it. So a player stands on a crate instead of falling through
256
+ * it. Turn it off for decoration nothing collides with, and for anything whose collider is
257
+ * expensive and whose contacts do not matter.
258
+ *
259
+ * Like `interpolate` and `predicted`, this is client-side simulation behavior, not server shape:
260
+ * it is not part of the canonical form or the schema hash, and `schemaFromCanonical` does not
261
+ * round-trip it. Requires `physics`, and does nothing without a client-side body factory.
262
+ */
263
+ readonly proxy?: boolean;
247
264
  /**
248
265
  * D22: this collection's instances are backed by rigid bodies. `body` names the fields the
249
266
  * simulation owns (read-only everywhere else), `intents` the owner-written inputs the step
@@ -330,6 +347,8 @@ interface CollectionDesc {
330
347
  readonly interpolate: boolean;
331
348
  /** D21: clients simulate non-owned bodies ahead. Client-side only — not in the hash. */
332
349
  readonly predicted: boolean;
350
+ /** D71: unpredicted instances get a kinematic proxy locally. Client-side only — not in the hash. */
351
+ readonly proxy: boolean;
333
352
  /** D22 body/intent field split, compiled. `undefined` for ordinary collections. In the hash. */
334
353
  readonly physics: PhysicsDesc | undefined;
335
354
  }
@@ -342,24 +361,56 @@ interface RpcDesc {
342
361
  /** `undefined` = void. */
343
362
  readonly returns: readonly FieldDesc[] | undefined;
344
363
  }
345
- interface SchemaOptions<Rpc extends RpcMap, Roles extends readonly string[]> {
364
+ /** Declared message shapes: name the fields one message of that name carries. */
365
+ type MessageMap = Readonly<Record<string, Fields>>;
366
+ /**
367
+ * One compiled message shape. `index` is its position in `schema.messages` (sorted by name) and
368
+ * is what rides the wire, so adding a message whose name sorts early is a breaking change in
369
+ * exactly the way adding an early-sorting collection is (see `diffSchemas`).
370
+ */
371
+ interface MessageDesc {
372
+ readonly name: string;
373
+ /** Index in `schema.messages` (sorted by name) = wire index. */
374
+ readonly index: number;
375
+ readonly fields: readonly FieldDesc[];
376
+ }
377
+ /**
378
+ * D70: the declared ceiling on message shapes. The wire index is a `u16` — this is the number a
379
+ * schema is refused at, deliberately far below what the encoding could carry, so that raising it
380
+ * later is additive rather than a wire change.
381
+ */
382
+ declare const MAX_MESSAGES = 256;
383
+ interface SchemaOptions<Rpc extends RpcMap, Roles extends readonly string[], Msgs extends MessageMap = MessageMap> {
346
384
  readonly rpc?: Rpc;
347
385
  readonly roles?: Roles;
386
+ /**
387
+ * D70: peer message shapes. `messages: { emote: { kind: enumOf('wave', 'laugh'), x: f32 } }`
388
+ * gives the client `room.messages.emote.send(target, value)` / `.on(cb)` and the room the same
389
+ * `send` plus a decoded sixth argument on `onMessage`. Part of the hash — a client and a room
390
+ * that disagree about a message shape disagree about the bytes — but emitted into the canonical
391
+ * form only when at least one is declared, so every schema written before this feature keeps
392
+ * the hash it had.
393
+ */
394
+ readonly messages?: Msgs;
348
395
  /** The public project id, written by `irtio init`. Identity, not shape: not part of the hash. */
349
396
  readonly project?: string;
350
397
  /** Internal: lets `@irtio/protocol` build the runtime-extended schema with built-in collections. */
351
398
  readonly allowReservedNames?: boolean;
352
399
  }
353
- interface Schema<Defs extends DefMap = DefMap, Rpc extends RpcMap = RpcMap, Roles extends readonly string[] = readonly string[]> {
400
+ interface Schema<Defs extends DefMap = DefMap, Rpc extends RpcMap = RpcMap, Roles extends readonly string[] = readonly string[], Msgs extends MessageMap = MessageMap> {
354
401
  readonly defs: Defs;
355
402
  readonly rpc: Rpc;
356
403
  readonly roles: Roles;
404
+ /** D70: the declared message map, as written. */
405
+ readonly messageDefs: Msgs;
357
406
  readonly project: string | undefined;
358
407
  /** Collections sorted by name; index = wire index. */
359
408
  readonly collections: readonly CollectionDesc[];
360
409
  readonly collection: (name: string) => CollectionDesc;
361
410
  /** Builder RPCs sorted by name; index = wire `rpcId` (protocol appends built-ins after). */
362
411
  readonly rpcs: readonly RpcDesc[];
412
+ /** D70: declared messages sorted by name; index = wire index. Empty when none are declared. */
413
+ readonly messages: readonly MessageDesc[];
363
414
  /** Canonical JSON (sorted keys, `canonicalVersion: 1`). */
364
415
  readonly canonical: string;
365
416
  /** SHA-256 of the canonical form, hex. */
@@ -367,8 +418,8 @@ interface Schema<Defs extends DefMap = DefMap, Rpc extends RpcMap = RpcMap, Role
367
418
  /** First 8 bytes of the hash (HELLO / snapshot / delta header). */
368
419
  readonly hash8: Uint8Array;
369
420
  }
370
- type AnySchema = Schema<any, any, any>;
371
- declare function defineSchema<const Defs extends DefMap, const Rpc extends RpcMap = {}, const Roles extends readonly string[] = readonly string[]>(defs: Defs, options?: SchemaOptions<Rpc, Roles>): Schema<Defs, Rpc, Roles>;
421
+ type AnySchema = Schema<any, any, any, any>;
422
+ declare function defineSchema<const Defs extends DefMap, const Rpc extends RpcMap = {}, const Roles extends readonly string[] = readonly string[], const Msgs extends MessageMap = {}>(defs: Defs, options?: SchemaOptions<Rpc, Roles, Msgs>): Schema<Defs, Rpc, Roles, Msgs>;
372
423
  declare const CANONICAL_VERSION = 1;
373
424
  declare function canonicalType(d: TypeDesc): Record<string, unknown>;
374
425
  /**
@@ -485,10 +536,42 @@ type DeepWritable<T> = T extends readonly (infer U)[] ? DeepWritable<U>[] : T ex
485
536
  } : T;
486
537
  /** The writable view of an instance the client owns (the client SDK hands these out). */
487
538
  type Owned<T> = DeepWritable<T>;
488
- type SchemaDefs<S> = S extends Schema<infer D, any, any> ? D : never;
489
- type SchemaRpc<S> = S extends Schema<any, infer R, any> ? R : never;
490
- type SchemaRoles<S> = S extends Schema<any, any, infer R> ? R : never;
539
+ type SchemaDefs<S> = S extends Schema<infer D, any, any, any> ? D : never;
540
+ type SchemaRpc<S> = S extends Schema<any, infer R, any, any> ? R : never;
541
+ type SchemaRoles<S> = S extends Schema<any, any, infer R, any> ? R : never;
491
542
  type RoleOf<S> = SchemaRoles<S>[number];
543
+ /** The declared message map of a schema (`{}` for a schema that declares none). */
544
+ type SchemaMessages<S> = S extends Schema<any, any, any, infer M> ? M : never;
545
+ /** Alias reading the way call sites do: `MessagesOf<typeof schema>`. */
546
+ type MessagesOf<S> = SchemaMessages<S>;
547
+ /** Every declared message name, as a union of string literals. */
548
+ type MessageNames<S> = keyof SchemaMessages<S> & string;
549
+ /**
550
+ * The plain TS value one message of name `N` carries — exactly what `send` takes and exactly
551
+ * what `on` hands back, `.opt` fields included.
552
+ */
553
+ type MessageValue<S, N extends MessageNames<S>> = SchemaMessages<S>[N] extends infer F ? F extends Fields ? InferFields<F> : never : never;
554
+ /**
555
+ * `room.messages` on both sides, given the target and sender types each side uses.
556
+ *
557
+ * The two sides differ only in those two: a client sends to a `MessageTarget` and hears from
558
+ * `'server' | clientId`; a room sends to its own `MessageTarget` and observes through
559
+ * `onMessage`. Sharing the mapped type is what keeps `send` and `on` agreeing about the shape.
560
+ */
561
+ type MessageChannels<S, Target, From, Unsub> = {
562
+ [K in MessageNames<S>]: {
563
+ send(target: Target, value: MessageValue<S, K>): void;
564
+ on(cb: (from: From, value: MessageValue<S, K>) => void): Unsub;
565
+ };
566
+ };
567
+ /** The server half: `send` only (a room observes through `onMessage`, never through `on`). */
568
+ type ServerMessageChannels<S, Target> = {
569
+ [K in MessageNames<S>]: {
570
+ send(target: Target, value: MessageValue<S, K>): void;
571
+ };
572
+ };
573
+ /** Guard for `MessageMap`-shaped generics at declaration sites. */
574
+ type AnyMessageMap = MessageMap;
492
575
  /** Plain instance type of an entity/singleton definition. */
493
576
  type InstanceOf<D> = D extends EntityDef<infer F, any> ? InferFields<F> : D extends SingletonDef<infer F, any> ? InferFields<F> : never;
494
577
  /** Init type (`add()` values) of an entity definition. */
@@ -702,7 +785,17 @@ declare class ByteReader {
702
785
  bytes(n: number): Uint8Array;
703
786
  /** varint length + raw bytes (a view). */
704
787
  blob(): Uint8Array;
705
- str(): string;
788
+ /**
789
+ * varint length + UTF-8 bytes.
790
+ *
791
+ * `max` is the declared byte ceiling for this string (`str(max)`, or `ID_MAX_BYTES` for a
792
+ * `ref`). Checked against the length prefix **before** the bytes are read, so a hostile length
793
+ * costs a comparison rather than a read — and so a decoder is as strict as the encoder, which
794
+ * has always refused an oversize value. The encoder's guarantee only ever covered our own
795
+ * encoders; this covers bytes that arrived from somewhere else (D70: a peer's typed message is
796
+ * decoded by every receiver).
797
+ */
798
+ str(max?: number): string;
706
799
  /** The rest of the buffer (a view). */
707
800
  rest(): Uint8Array;
708
801
  }
@@ -917,4 +1010,4 @@ interface SchemaChange {
917
1010
  /** Compares two compiled schemas and returns every classified change, sorted by path then code. */
918
1011
  declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
919
1012
 
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 };
1013
+ export { type AddOptions, type AnyDef, type AnyMessageMap, 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, MAX_MESSAGES, type MessageChannels, type MessageDesc, type MessageMap, type MessageNames, type MessageValue, type MessagesOf, 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 SchemaMessages, type SchemaOptions, type SchemaRoles, type SchemaRpc, type ServerCallProxy, type ServerMessageChannels, 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
@@ -542,6 +542,7 @@ function makeRpc(direction, spec) {
542
542
  }
543
543
  var RESERVED_RPC_NAMES = ["requestOwnership"];
544
544
  var RESERVED_COLLECTION_NAMES = ["clients"];
545
+ var MAX_MESSAGES = 256;
545
546
  function defineSchema(defs, options = {}) {
546
547
  const names = Object.keys(defs);
547
548
  for (const n of names) {
@@ -559,6 +560,11 @@ function defineSchema(defs, options = {}) {
559
560
  `${name}: predicted: true needs physics \u2014 only body-backed collections can be simulated ahead`
560
561
  );
561
562
  }
563
+ if (o.proxy !== void 0 && !o.physics) {
564
+ throw new Error(
565
+ `${name}: proxy needs physics \u2014 only body-backed collections get a body in a client's local world`
566
+ );
567
+ }
562
568
  return {
563
569
  name,
564
570
  index,
@@ -577,6 +583,7 @@ function defineSchema(defs, options = {}) {
577
583
  } : void 0,
578
584
  interpolate: o.interpolate !== false,
579
585
  predicted: o.predicted === true,
586
+ proxy: o.proxy !== false,
580
587
  physics: o.physics ? compilePhysics(name, def.kind, fields, o.physics) : void 0
581
588
  };
582
589
  });
@@ -608,13 +615,46 @@ function defineSchema(defs, options = {}) {
608
615
  });
609
616
  const roles = options.roles ?? [];
610
617
  if (new Set(roles).size !== roles.length) throw new Error("roles must be unique");
611
- const canonical = canonicalize({ collections, rpcs, roles });
618
+ const messageMap = options.messages ?? {};
619
+ const messageNames = Object.keys(messageMap);
620
+ if (messageNames.length > MAX_MESSAGES) {
621
+ throw new Error(
622
+ `at most ${MAX_MESSAGES} messages per schema, got ${messageNames.length} \u2014 a message index rides the wire, and this ceiling is what a client can be sure of`
623
+ );
624
+ }
625
+ const rpcNameSet = new Set(Object.keys(rpcMap));
626
+ const collectionNameSet = new Set(names);
627
+ for (const n of messageNames) {
628
+ assertFieldName(n);
629
+ if (collectionNameSet.has(n)) {
630
+ throw new Error(
631
+ `message name ${JSON.stringify(n)} is already a collection in this schema \u2014 pick another`
632
+ );
633
+ }
634
+ if (rpcNameSet.has(n)) {
635
+ throw new Error(
636
+ `message name ${JSON.stringify(n)} is already an RPC in this schema \u2014 pick another`
637
+ );
638
+ }
639
+ const fields = messageMap[n];
640
+ if (Object.keys(fields).length > 255) {
641
+ throw new Error(`message ${JSON.stringify(n)}: at most 255 fields`);
642
+ }
643
+ for (const f of Object.keys(fields)) assertFieldName(f);
644
+ }
645
+ const messages = [...messageNames].sort().map((name, index) => {
646
+ const fields = compileFields(messageMap[name]);
647
+ for (const f of fields) checkRefs(f.type, entityNames, `message ${name}.${f.name}`);
648
+ return { name, index, fields };
649
+ });
650
+ const canonical = canonicalize({ collections, rpcs, roles, messages });
612
651
  const hashBytes = sha256(new TextEncoder().encode(canonical));
613
652
  const byName = new Map(collections.map((c) => [c.name, c]));
614
653
  return {
615
654
  defs,
616
655
  rpc: rpcMap,
617
656
  roles,
657
+ messageDefs: messageMap,
618
658
  project: options.project,
619
659
  collections,
620
660
  collection: (name) => {
@@ -623,6 +663,7 @@ function defineSchema(defs, options = {}) {
623
663
  return c;
624
664
  },
625
665
  rpcs,
666
+ messages,
626
667
  canonical,
627
668
  hash: toHex(hashBytes),
628
669
  hash8: hashBytes.slice(0, 8)
@@ -698,6 +739,10 @@ function canonicalize(s) {
698
739
  // Emitted only when declared: `stableStringify` drops `undefined`, so every schema written
699
740
  // before D22 keeps the byte-identical canonical form — and therefore the hash — it had.
700
741
  physics: c.physics ? canonicalPhysics(c.physics) : void 0
742
+ // Deliberately absent, and this is the list: `interpolate` (D20), `predicted` (D21) and
743
+ // `proxy` (D71) are all decisions about what one client does with a body it was sent, not
744
+ // about the shape of the world. A room and a client that disagree about any of them still
745
+ // agree about every byte on the wire, so none of them may move the hash.
701
746
  })),
702
747
  rpcs: s.rpcs.map((r) => ({
703
748
  name: r.name,
@@ -705,7 +750,12 @@ function canonicalize(s) {
705
750
  params: canonicalFields(r.params),
706
751
  returns: r.returns ? canonicalFields(r.returns) : null
707
752
  })),
708
- roles: [...s.roles]
753
+ roles: [...s.roles],
754
+ // D70: emitted only when at least one message is declared. `stableStringify` drops
755
+ // `undefined`, so every schema written before typed messages keeps the byte-identical
756
+ // canonical form — and therefore the hash — it had, and no deployed project is reclassified.
757
+ // Same rule as `grid` and `physics` above, and for the same reason.
758
+ messages: s.messages.length > 0 ? s.messages.map((m) => ({ name: m.name, fields: canonicalFields(m.fields) })) : void 0
709
759
  };
710
760
  return stableStringify(doc);
711
761
  }
@@ -859,9 +909,17 @@ function schemaFromCanonical(json, options = {}) {
859
909
  if (typeof role !== "string") throw new Error("schemaFromCanonical: roles must be strings");
860
910
  roles.push(role);
861
911
  }
912
+ const messages = {};
913
+ if (d.messages !== void 0 && d.messages !== null) {
914
+ for (const m of arrayAt(d, "messages")) {
915
+ const name = stringAt(m, "name");
916
+ messages[name] = fieldsOf(arrayAt(m, "fields"), `message ${name}`);
917
+ }
918
+ }
862
919
  const schema = defineSchema(defs, {
863
920
  rpc,
864
921
  roles,
922
+ ...Object.keys(messages).length > 0 ? { messages } : {},
865
923
  // A canonical form produced by `withBuiltins` names the reserved `clients` collection; a
866
924
  // reader must be able to replay whatever it is handed.
867
925
  allowReservedNames: true,
@@ -1377,8 +1435,21 @@ var ByteReader = class {
1377
1435
  blob() {
1378
1436
  return this.bytes(this.varint());
1379
1437
  }
1380
- str() {
1381
- return decoder.decode(this.blob());
1438
+ /**
1439
+ * varint length + UTF-8 bytes.
1440
+ *
1441
+ * `max` is the declared byte ceiling for this string (`str(max)`, or `ID_MAX_BYTES` for a
1442
+ * `ref`). Checked against the length prefix **before** the bytes are read, so a hostile length
1443
+ * costs a comparison rather than a read — and so a decoder is as strict as the encoder, which
1444
+ * has always refused an oversize value. The encoder's guarantee only ever covered our own
1445
+ * encoders; this covers bytes that arrived from somewhere else (D70: a peer's typed message is
1446
+ * decoded by every receiver).
1447
+ */
1448
+ str(max) {
1449
+ if (max === void 0) return decoder.decode(this.blob());
1450
+ const n = this.varint();
1451
+ if (n > max) throw new Error(`string exceeds ${max} UTF-8 bytes (${n})`);
1452
+ return decoder.decode(this.bytes(n));
1382
1453
  }
1383
1454
  /** The rest of the buffer (a view). */
1384
1455
  rest() {
@@ -1540,7 +1611,7 @@ function readValue(r, desc) {
1540
1611
  return r.f64();
1541
1612
  case "str":
1542
1613
  case "ref":
1543
- return r.str();
1614
+ return r.str(desc.kind === "str" ? desc.max : ID_MAX_BYTES);
1544
1615
  case "enum": {
1545
1616
  const i = r.u8();
1546
1617
  const v = desc.values[i];
@@ -2516,6 +2587,11 @@ function diffSchemas(oldSchema, newSchema) {
2516
2587
  );
2517
2588
  diffRpcs(oldSchema.rpcs, newSchema.rpcs, changes);
2518
2589
  diffRoles(oldSchema.roles, newSchema.roles, changes);
2590
+ diffMessages(
2591
+ oldSchema.messages ?? [],
2592
+ newSchema.messages ?? [],
2593
+ changes
2594
+ );
2519
2595
  changes.sort(
2520
2596
  (a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
2521
2597
  );
@@ -2652,6 +2728,43 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
2652
2728
  diffFields(oldR.returns ?? [], newR.returns ?? [], `rpc.${oldR.name}.returns`, changes);
2653
2729
  }
2654
2730
  }
2731
+ function diffMessages(oldMessages, newMessages, changes) {
2732
+ const oldByName = new Map(oldMessages.map((m) => [m.name, m]));
2733
+ const newByName = new Map(newMessages.map((m) => [m.name, m]));
2734
+ for (const m of oldMessages) {
2735
+ if (!newByName.has(m.name)) {
2736
+ changes.push({
2737
+ kind: "breaking",
2738
+ code: "message.removed",
2739
+ path: `message.${m.name}`,
2740
+ message: `message ${m.name}: was removed \u2014 peers still sending it would be dropped`
2741
+ });
2742
+ }
2743
+ }
2744
+ for (const m of newMessages) {
2745
+ if (!oldByName.has(m.name)) {
2746
+ changes.push({
2747
+ kind: "additive",
2748
+ code: "message.added",
2749
+ path: `message.${m.name}`,
2750
+ message: `message ${m.name}: new message shape added`
2751
+ });
2752
+ }
2753
+ }
2754
+ for (const oldM of oldMessages) {
2755
+ const newM = newByName.get(oldM.name);
2756
+ if (!newM) continue;
2757
+ if (oldM.index !== newM.index) {
2758
+ changes.push({
2759
+ kind: "breaking",
2760
+ code: "message.reordered",
2761
+ path: `message.${oldM.name}`,
2762
+ message: `message ${oldM.name}: wire index changed (${oldM.index} -> ${newM.index}) \u2014 messages are addressed positionally, so peers would decode this one as another (breaking)`
2763
+ });
2764
+ }
2765
+ diffFields(oldM.fields, newM.fields, `message.${oldM.name}`, changes);
2766
+ }
2767
+ }
2655
2768
  function diffRoles(oldRoles, newRoles, changes) {
2656
2769
  const oldSet = new Set(oldRoles);
2657
2770
  const newSet = new Set(newRoles);
@@ -2892,6 +3005,7 @@ export {
2892
3005
  CANONICAL_VERSION,
2893
3006
  EntityCollection,
2894
3007
  ID_MAX_BYTES,
3008
+ MAX_MESSAGES,
2895
3009
  PHYSICS_BODY_CHANNELS,
2896
3010
  RESERVED_COLLECTION_NAMES,
2897
3011
  RESERVED_RPC_NAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/schema",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "irtio schema DSL, type inference, canonical hash, codec, change tracking, and schema diff",
5
5
  "license": "MIT",
6
6
  "publishConfig": {