@irtio/schema 0.11.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +157 -10
  2. package/dist/index.js +196 -37
  3. package/package.json +6 -1
package/dist/index.d.ts CHANGED
@@ -93,6 +93,7 @@ type InitFields<F extends Fields> = Simplify<{
93
93
  [K in InitOptKeys<F>]?: Infer<F[K]> | undefined;
94
94
  }>;
95
95
  declare function describeType(d: TypeDesc): string;
96
+ declare function utf8Length(s: string): number;
96
97
  /** Entity ids are strings of at most this many UTF-8 bytes (`str(32)` on the wire). */
97
98
  declare const ID_MAX_BYTES = 32;
98
99
  /**
@@ -279,7 +280,12 @@ declare function ownershipFromCanonical(at: string, raw: unknown): OwnershipOpti
279
280
  * (ordered collection descriptors, sorted RPC table, canonical form, hash).
280
281
  */
281
282
 
282
- type Visibility = 'all' | 'role' | 'spatial-grid';
283
+ /**
284
+ * `'server'` is the room's own private state: no client role sees it, no client may write it, and
285
+ * it never reaches the wire. It is the first-class spelling of what `visibility: 'role'` with an
286
+ * empty `roles` list has always done by accident, and it takes no `roles`.
287
+ */
288
+ type Visibility = 'all' | 'role' | 'spatial-grid' | 'server';
283
289
  interface GridOptions<K extends string = string> {
284
290
  /** Numeric fields containing the entity's world position. */
285
291
  readonly x: K;
@@ -296,7 +302,10 @@ interface GridDesc extends GridOptions<string> {
296
302
  interface EntityOptions {
297
303
  /** `true` → never client-owned; `DeepReadonly` on the client at compile time. */
298
304
  readonly serverOwned?: boolean;
299
- /** `'all'` (default) | `'role'` (per-role views, needs `roles`) | `'spatial-grid'` (per-client AOI). */
305
+ /**
306
+ * `'all'` (default) | `'role'` (per-role views, needs `roles`) | `'spatial-grid'` (per-client
307
+ * AOI) | `'server'` (room-private: no client sees or writes it).
308
+ */
300
309
  readonly visibility?: Visibility;
301
310
  /** With `visibility: 'role'`: the roles that see this collection. */
302
311
  readonly roles?: readonly string[];
@@ -364,6 +373,27 @@ interface EntityOptions {
364
373
  * unowned, and a leaving player's records keep their (now absent) owner.
365
374
  */
366
375
  readonly ownership?: OwnershipOptions;
376
+ /**
377
+ * Fields of a client-owned record that only the server may write.
378
+ *
379
+ * Ownership is per record, so the server's own facts about a client-owned entity — granted
380
+ * upgrades, a validated score, a cooldown the room controls — have had to live in a parallel
381
+ * collection keyed by the same id. This puts them on the record and refuses the client's writes
382
+ * to them by name: the leaf goes back to the value it had before any validator runs, the client
383
+ * is corrected, and the rest of the same write still lands.
384
+ *
385
+ * Server behaviour, so it is part of the canonical form and the hash — a client and a room that
386
+ * disagree about who writes `upgrades` disagree about the world. Emitted into the canonical form
387
+ * only when declared, so every schema written before this keeps the hash it had.
388
+ *
389
+ * Entity-only, and meaningless on a `serverOwned` collection (where every field already is);
390
+ * both are refused. On a `physics` collection the intent list already answers the question, so a
391
+ * field named by both is refused too.
392
+ *
393
+ * There is deliberately no dynamic form. A per-field *owner id* would have to ride the wire per
394
+ * field per record, and the case that needs one is served by a second collection.
395
+ */
396
+ readonly serverFields?: readonly string[];
367
397
  }
368
398
  interface EntityDef<F extends Fields = Fields, O extends EntityOptions = EntityOptions> {
369
399
  readonly kind: 'entity';
@@ -418,6 +448,12 @@ declare function client<P extends Fields = {}, R extends Fields = Fields>(spec:
418
448
  }): RpcDef<'client', P, R>;
419
449
  /** RPC names a builder may not declare (built-ins live in `@irtio/protocol`). */
420
450
  declare const RESERVED_RPC_NAMES: readonly string[];
451
+ /**
452
+ * The prefix irtio's built-in RPC names carry (`$ownership.ask`), refused to a builder here and to
453
+ * a caller in `@irtio/protocol`'s `isReservedRpcName`. Stated twice because the dependency runs
454
+ * schema → protocol and never the other way.
455
+ */
456
+ declare const RESERVED_RPC_PREFIX = "$";
421
457
  /** Collection names a builder may not declare (the runtime merges built-in state under them). */
422
458
  declare const RESERVED_COLLECTION_NAMES: readonly string[];
423
459
  interface FieldDesc {
@@ -452,10 +488,21 @@ interface CollectionDesc {
452
488
  readonly clientCreate: boolean;
453
489
  /** D77: the compiled transfer and leave rules. `undefined` when none were declared. In the hash. */
454
490
  readonly ownership: OwnershipDesc | undefined;
491
+ /**
492
+ * M7 pf3 (e): the field names only the server may write, sorted, or `undefined` when none were
493
+ * declared. In the hash when present. A `Set` because `writes.ts` asks the membership question
494
+ * once per written leaf.
495
+ */
496
+ readonly serverFields: ReadonlySet<string> | undefined;
455
497
  }
456
498
  interface RpcDesc {
457
499
  readonly name: string;
458
- /** Index in `schema.rpcs` (sorted by name). Built-ins are appended by `@irtio/protocol`. */
500
+ /**
501
+ * The wire id: this RPC's position in **declaration order**, 0-based (M7 pf2). It is not a
502
+ * sort — the order the developer wrote the `rpc` object in is the order of the id space, and
503
+ * moving a declaration moves an id. `@irtio/protocol`'s built-ins live in a reserved range at
504
+ * the top of the u16 and never collide with these.
505
+ */
459
506
  readonly index: number;
460
507
  readonly direction: RpcDirection;
461
508
  readonly params: readonly FieldDesc[];
@@ -465,13 +512,13 @@ interface RpcDesc {
465
512
  /** Declared message shapes: name → the fields one message of that name carries. */
466
513
  type MessageMap = Readonly<Record<string, Fields>>;
467
514
  /**
468
- * One compiled message shape. `index` is its position in `schema.messages` (sorted by name) and
469
- * is what rides the wire, so adding a message whose name sorts early is a breaking change in
470
- * exactly the way adding an early-sorting collection is (see `diffSchemas`).
515
+ * One compiled message shape. `index` is its position in `schema.messages` in **declaration
516
+ * order** (M7 pf2) and is what rides the wire, so appending a message is additive and moving an
517
+ * existing declaration is breaking, in exactly the way it is for a collection (see `diffSchemas`).
471
518
  */
472
519
  interface MessageDesc {
473
520
  readonly name: string;
474
- /** Index in `schema.messages` (sorted by name) = wire index. */
521
+ /** Index in `schema.messages` (declaration order) = wire index. */
475
522
  readonly index: number;
476
523
  readonly fields: readonly FieldDesc[];
477
524
  }
@@ -481,6 +528,13 @@ interface MessageDesc {
481
528
  * later is additive rather than a wire change.
482
529
  */
483
530
  declare const MAX_MESSAGES = 256;
531
+ /**
532
+ * M7 pf2: the ceiling on builder-declared RPCs. `Call.rpcId` is a `u16`, and `@irtio/protocol`
533
+ * reserves the top of that space for irtio's built-ins (65535 downward). This cap is what keeps
534
+ * the two ranges from ever meeting, with thousands of ids of headroom on both sides — a schema
535
+ * that hits it has a problem no id scheme can fix.
536
+ */
537
+ declare const MAX_USER_RPCS = 60000;
484
538
  interface SchemaOptions<Rpc extends RpcMap, Roles extends readonly string[], Msgs extends MessageMap = MessageMap> {
485
539
  readonly rpc?: Rpc;
486
540
  readonly roles?: Roles;
@@ -589,6 +643,8 @@ interface Collection<T, Init = T> extends ReadonlyCollection<T> {
589
643
  add(id: string, values: Init, options?: AddOptions): T;
590
644
  remove(id: string): boolean;
591
645
  setOwner(id: string, owner: string): void;
646
+ /** M7 pf3 (d): make this collection hold exactly `entries`. See {@link reconcileCollection}. */
647
+ reconcile(entries: Iterable<readonly [string, Init]>): void;
592
648
  [Symbol.iterator](): IterableIterator<readonly [string, T]>;
593
649
  }
594
650
  interface Record_<T> {
@@ -606,10 +662,43 @@ declare class EntityCollection<T extends object = any, Init = T> implements Coll
606
662
  has(id: string): boolean;
607
663
  setOwner(id: string, owner: string): void;
608
664
  ownerOf(id: string): string | undefined;
665
+ reconcile(entries: Iterable<readonly [string, Init]>): void;
609
666
  get size(): number;
610
667
  ids(): IterableIterator<string>;
611
668
  [Symbol.iterator](): IterableIterator<readonly [string, T]>;
612
669
  }
670
+ /**
671
+ * M7 pf3 (d): make `coll` hold exactly the ids `entries` yields, and nothing else.
672
+ *
673
+ * The shape a transient set wants — fires, pings, scorch marks, anything recomputed wholesale
674
+ * every tick. What it replaces is an `add`/`remove` bookkeeping pass plus a scratch `Set`, written
675
+ * again in every room that has one, and got subtly wrong the first time in most of them.
676
+ *
677
+ * It is a **thin diff over the existing methods**, not a second write path:
678
+ *
679
+ * - an id in `entries` that the collection does not have is `add`ed, exactly as if you had called
680
+ * `add` yourself (defaults filled, owner the server);
681
+ * - an id it already has keeps its record object, its owner, and its physics body: the values in
682
+ * `entries` are **assigned onto the existing record**, field by field, so a value that did not
683
+ * really change does not dirty and a body-backed row is not torn down and rebuilt;
684
+ * - an id the collection has and `entries` does not is `remove`d.
685
+ *
686
+ * That is the whole of the difference between this and a loop of `add()`s: `add` installs a *new*
687
+ * record object, which re-dirties every field and (on a physics collection) rebuilds the body at
688
+ * its spawn pose. `reconcile` is for the set that mostly stays the same.
689
+ *
690
+ * On a tracked collection every one of those steps goes through the tracking proxies, so the
691
+ * dirty set comes out identical to the one hand-written bookkeeping would have produced.
692
+ *
693
+ * Partial `entries` values are honoured on an existing row: a pair of `['a', { x: 3 }]` assigns
694
+ * `x` and leaves every other field of `a` alone. On a *new* row the same pair goes through `add`,
695
+ * where the collection's `.default()`/`.opt` rules fill the rest and a missing required field
696
+ * throws — the same asymmetry `add` has always had.
697
+ *
698
+ * Removals happen after the additions, so an id present in `entries` is never removed and
699
+ * re-added by one call.
700
+ */
701
+ declare function reconcileCollection<T extends object, Init>(coll: Collection<T, Init>, desc: CollectionDesc, entries: Iterable<readonly [string, Init]>): void;
613
702
  /** Builds a full record from init values: every field present, defaults applied, f32 frounded. */
614
703
  declare function normalizeRecord(desc: CollectionDesc, values: Record<string, unknown>): Record<string, unknown>;
615
704
  /** A record with every field at its default. */
@@ -706,6 +795,8 @@ type RolesOf<O extends EntityOptions> = O extends {
706
795
  } ? R : never;
707
796
  /** Is collection with options `O` visible to `Role`? */
708
797
  type VisibleTo<O extends EntityOptions, Role extends string> = O extends {
798
+ visibility: 'server';
799
+ } ? false : O extends {
709
800
  visibility: 'role';
710
801
  } ? [Role] extends [RolesOf<O>] ? true : false : true;
711
802
  type VisibleKeys<S, Role extends string> = {
@@ -923,6 +1014,12 @@ declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
923
1014
  *
924
1015
  * Encoding validates: oversize strings/lists, out-of-range or non-integer ints, wrong types,
925
1016
  * missing required values and unknown enum members all throw with the field path.
1017
+ *
1018
+ * Decoding validates what a peer can lie about: an oversize string, an over-max list, an unknown
1019
+ * enum index and a non-finite `f32`/`f64` all throw, which every caller of this codec already
1020
+ * surfaces as a refused frame rather than as a value in state. The single exception is
1021
+ * `decodeSnapshot`, which coerces a non-finite float to the field default and warns, because
1022
+ * those bytes can be a room's own save and refusing them loses the room; see the comment there.
926
1023
  */
927
1024
 
928
1025
  /**
@@ -943,8 +1040,22 @@ interface ValueSink {
943
1040
  }
944
1041
  /** Writes one value in snapshot form (presence byte first when the descriptor is `.opt`). */
945
1042
  declare function writeValue(w: ValueSink, desc: TypeDesc, v: unknown, path?: string): void;
1043
+ /**
1044
+ * How a decode answers a non-finite `f32`/`f64` it reads off the wire.
1045
+ *
1046
+ * The default — and the only answer on the delta/WRITE path — is to throw, which is what makes a
1047
+ * peer-authored NaN an ordinary refused frame. {@link decodeSnapshot} passes a `coerce` reporter
1048
+ * instead; see the comment there for why the two paths must differ.
1049
+ */
1050
+ interface ReadOptions {
1051
+ /**
1052
+ * When set, a non-finite float is replaced by the field's default and reported here instead of
1053
+ * throwing. `path` is the field path, `value` the number that was read.
1054
+ */
1055
+ readonly onNonFinite?: (path: string, value: number) => void;
1056
+ }
946
1057
  /** Reads one value in snapshot form. */
947
- declare function readValue(r: ByteReader, desc: TypeDesc): unknown;
1058
+ declare function readValue(r: ByteReader, desc: TypeDesc, opts?: ReadOptions, path?: string): unknown;
948
1059
  /** Snapshot-form record encoding — used for RPC params and returns. */
949
1060
  declare function encodeFields(fields: readonly FieldDesc[], value: Record<string, unknown>): Uint8Array;
950
1061
  /** Decodes a snapshot-form record. Accepts raw bytes or a positioned `ByteReader`. */
@@ -964,7 +1075,29 @@ interface DecodedSnapshot {
964
1075
  readonly hash8: Uint8Array;
965
1076
  readonly state: PlainState;
966
1077
  }
967
- declare function decodeSnapshot(schema: AnySchema, bytes: Uint8Array): DecodedSnapshot;
1078
+ interface DecodeSnapshotOptions {
1079
+ /**
1080
+ * Where a non-finite float found in the snapshot is reported. Defaults to a `console.warn`.
1081
+ * The field is restored as its declared default either way.
1082
+ */
1083
+ readonly onNonFinite?: (path: string, value: number) => void;
1084
+ }
1085
+ /**
1086
+ * Decodes a whole-state snapshot: a join payload, or the codec section of a room save.
1087
+ *
1088
+ * **Why this path tolerates a non-finite float where the delta path refuses one.** A delta is a
1089
+ * live frame from a peer, so a NaN in it is an attack or a bug happening *now* and refusing the
1090
+ * frame costs one frame. A snapshot is also how a hibernated room comes back, and those bytes
1091
+ * were written by *us* — possibly months ago, before `normalizeValue` re-checked after its
1092
+ * fround and before `writeValue` checked at all. Applying the strict rule to them means a save
1093
+ * containing one Infinity is a room that can never be restored again: every wake throws, and
1094
+ * since the save is the room, the room is gone. Weighed against that, coercing the one poisoned
1095
+ * field to its declared default and saying so loudly is plainly the better trade — the room
1096
+ * comes back, one number is wrong in a way the operator has been told about, and room code fixes
1097
+ * or overwrites it on the next tick. This deliberately does *not* extend to `decodeDelta` or the
1098
+ * WRITE path, where the bytes are a peer's and refusing them is free.
1099
+ */
1100
+ declare function decodeSnapshot(schema: AnySchema, bytes: Uint8Array, options?: DecodeSnapshotOptions): DecodedSnapshot;
968
1101
  type DeltaOp = {
969
1102
  readonly op: 'add';
970
1103
  readonly id: string;
@@ -1110,5 +1243,19 @@ interface SchemaChange {
1110
1243
  }
1111
1244
  /** Compares two compiled schemas and returns every classified change, sorted by path then code. */
1112
1245
  declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
1246
+ /**
1247
+ * True when every breaking change in `changes` lives under the RPC table (`rpc` / `rpc.*`) or the
1248
+ * message table (`message.*`), and there is at least one. Such a change breaks connected clients
1249
+ * (the version fence reconnects them) but touches nothing a snapshot stores, so a deploy needs
1250
+ * `--allow-breaking` and no migration: a chain step without one decodes and re-encodes losslessly.
1251
+ *
1252
+ * M7 pf2 added the message half. Messages are transient peer packets — the snapshot encoder has
1253
+ * no reference to `schema.messages` at all — so a message-only break has never had anything to
1254
+ * migrate; before pf2 nobody noticed, because a message index only moved when a name was inserted
1255
+ * early. Declaration-order indices make moves ordinary, and the pf2 transition itself renumbers
1256
+ * messages for every project that declares more than one. Demanding a hand-written identity
1257
+ * migration for a packet nothing stores would be a lie about what the deploy costs.
1258
+ */
1259
+ declare function breakingIsWireOnly(changes: readonly SchemaChange[]): boolean;
1113
1260
 
1114
- 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, type OwnershipDesc, type OwnershipLeave, type OwnershipLeaveAction, type OwnershipOptions, type OwnershipPosition, type OwnershipTransfer, 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, canonicalOwnership, canonicalPhysics, canonicalType, channelOf2d, client, cloneValue, collectionDirty, compileOwnership, 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, ownershipFromCanonical, parseHold, physicsFromCanonical, readValue, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, validateForDeploy, validateValue, walkDelta, walkSnapshot, writeValue };
1261
+ 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 DecodeSnapshotOptions, 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, MAX_USER_RPCS, type MessageChannels, type MessageDesc, type MessageMap, type MessageNames, type MessageValue, type MessagesOf, type OwnableKeys, type Owned, type OwnershipDesc, type OwnershipLeave, type OwnershipLeaveAction, type OwnershipOptions, type OwnershipPosition, type OwnershipTransfer, PHYSICS_BODY_CHANNELS, type PhysicsBodyChannel, type PhysicsBodyMap, type PhysicsDesc, type PhysicsKeys, type PlainState, RESERVED_COLLECTION_NAMES, RESERVED_RPC_NAMES, RESERVED_RPC_PREFIX, type ReadOptions, 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, breakingIsWireOnly, bytesEqual, canonicalOwnership, canonicalPhysics, canonicalType, channelOf2d, client, cloneValue, collectionDirty, compileOwnership, 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, ownershipFromCanonical, parseHold, physicsFromCanonical, readValue, reconcileCollection, ref, schemaFromCanonical, server, sha256, sha256Hex, singleton, stableStringify, str, struct, toHex, track, u16, u32, u8, utf8Length, validateForDeploy, validateValue, walkDelta, walkSnapshot, writeValue };
package/dist/index.js CHANGED
@@ -112,7 +112,8 @@ function validateValue(d, v, path = "") {
112
112
  }
113
113
  case "f32":
114
114
  case "f64":
115
- return typeof v === "number" ? null : `${at} must be a number`;
115
+ if (typeof v !== "number") return `${at} must be a number`;
116
+ return Number.isFinite(v) ? null : `${at} must be a finite number (${d.kind})`;
116
117
  case "str":
117
118
  case "ref": {
118
119
  if (typeof v !== "string") return `${at} must be a string`;
@@ -187,7 +188,11 @@ function normalizeValue(d, v, path = "") {
187
188
  switch (d.kind) {
188
189
  case "f32": {
189
190
  if (typeof v !== "number") throw new Error(`${at} must be a number`);
190
- return Math.fround(v);
191
+ if (!Number.isFinite(v)) throw new Error(`${at} must be a finite number (f32)`);
192
+ const f = Math.fround(v);
193
+ if (!Number.isFinite(f))
194
+ throw new Error(`${at} overflows f32 (${v} rounds to ${f > 0 ? "Infinity" : "-Infinity"})`);
195
+ return f;
191
196
  }
192
197
  case "list": {
193
198
  if (!Array.isArray(v)) throw new Error(`${at} must be an array`);
@@ -657,6 +662,48 @@ function singleton(fields, options) {
657
662
  checkFields(fields);
658
663
  return { kind: "singleton", fields, options: options ?? {} };
659
664
  }
665
+ function compileServerFields(name, kind, fields, o) {
666
+ const declared = o.serverFields;
667
+ if (declared === void 0) return void 0;
668
+ if (!Array.isArray(declared)) {
669
+ throw new Error(`${name}: serverFields must be an array of field names`);
670
+ }
671
+ if (kind !== "entity") {
672
+ throw new Error(
673
+ `${name}: serverFields is entity-only \u2014 a client never writes a singleton, so every field of one is already server-only`
674
+ );
675
+ }
676
+ if (o.serverOwned === true) {
677
+ throw new Error(
678
+ `${name}: serverFields on a serverOwned collection says nothing \u2014 serverOwned already means no client writes any field of it`
679
+ );
680
+ }
681
+ if (declared.length === 0) {
682
+ throw new Error(`${name}: serverFields is empty; leave it off instead`);
683
+ }
684
+ const known = new Set(fields.map((f) => f.name));
685
+ const out = /* @__PURE__ */ new Set();
686
+ for (const field of declared) {
687
+ if (typeof field !== "string" || !known.has(field)) {
688
+ throw new Error(
689
+ `${name}: serverFields names ${JSON.stringify(field)}, which is not a field of ${name}`
690
+ );
691
+ }
692
+ if (out.has(field)) throw new Error(`${name}: serverFields names ${field} twice`);
693
+ out.add(field);
694
+ }
695
+ if (out.size === fields.length) {
696
+ throw new Error(
697
+ `${name}: serverFields names every field \u2014 that is serverOwned: true, which says it once`
698
+ );
699
+ }
700
+ if (o.physics) {
701
+ throw new Error(
702
+ `${name}: serverFields says nothing on a physics collection \u2014 the owner of a body-backed record writes its declared intents and nothing else. Take the field out of physics.intents instead`
703
+ );
704
+ }
705
+ return new Set([...out].sort());
706
+ }
660
707
  function checkFields(fields) {
661
708
  const names = Object.keys(fields);
662
709
  if (names.length === 0) throw new Error("entity/singleton needs at least one field");
@@ -676,8 +723,10 @@ function makeRpc(direction, spec) {
676
723
  return { kind: "rpc", direction, params, returns: spec.returns };
677
724
  }
678
725
  var RESERVED_RPC_NAMES = ["requestOwnership"];
726
+ var RESERVED_RPC_PREFIX = "$";
679
727
  var RESERVED_COLLECTION_NAMES = ["clients"];
680
728
  var MAX_MESSAGES = 256;
729
+ var MAX_USER_RPCS = 6e4;
681
730
  function defineSchema(defs, options = {}) {
682
731
  const names = Object.keys(defs);
683
732
  for (const n of names) {
@@ -713,6 +762,7 @@ function defineSchema(defs, options = {}) {
713
762
  );
714
763
  }
715
764
  const ownership = o.ownership || perPlayer ? compileOwnership(name, def.kind, fields, o.ownership ?? {}, perPlayer) : void 0;
765
+ const serverFields = compileServerFields(name, def.kind, fields, o);
716
766
  return {
717
767
  name,
718
768
  index,
@@ -720,6 +770,7 @@ function defineSchema(defs, options = {}) {
720
770
  fields,
721
771
  fieldIndex: new Map(fields.map((f) => [f.name, f.index])),
722
772
  serverOwned: o.serverOwned === true,
773
+ serverFields,
723
774
  perPlayer,
724
775
  clientCreate: o.clientCreate === true,
725
776
  ownership,
@@ -763,13 +814,23 @@ function defineSchema(defs, options = {}) {
763
814
  }
764
815
  }
765
816
  const rpcMap = options.rpc ?? {};
817
+ if (Object.keys(rpcMap).length > MAX_USER_RPCS) {
818
+ throw new Error(
819
+ `at most ${MAX_USER_RPCS} RPCs per schema, got ${Object.keys(rpcMap).length} \u2014 an rpcId is a u16 and the top of that space is reserved for irtio's built-in RPCs`
820
+ );
821
+ }
766
822
  for (const n of Object.keys(rpcMap)) {
767
823
  assertFieldName(n);
768
824
  if (RESERVED_RPC_NAMES.includes(n)) {
769
825
  throw new Error(`RPC name ${JSON.stringify(n)} is reserved by irtio (built-in RPC)`);
770
826
  }
827
+ if (n.startsWith(RESERVED_RPC_PREFIX)) {
828
+ throw new Error(
829
+ `RPC name ${JSON.stringify(n)} starts with ${JSON.stringify(RESERVED_RPC_PREFIX)}, which is reserved for irtio's built-in RPCs \u2014 pick another name`
830
+ );
831
+ }
771
832
  }
772
- const rpcs = Object.keys(rpcMap).sort().map((name, index) => {
833
+ const rpcs = Object.keys(rpcMap).map((name, index) => {
773
834
  const r = rpcMap[name];
774
835
  for (const f of Object.values(r.params))
775
836
  checkRefs(f.desc, entityNames, `rpc ${name} params`);
@@ -813,7 +874,7 @@ function defineSchema(defs, options = {}) {
813
874
  }
814
875
  for (const f of Object.keys(fields)) assertFieldName(f);
815
876
  }
816
- const messages = [...messageNames].sort().map((name, index) => {
877
+ const messages = messageNames.map((name, index) => {
817
878
  const fields = compileFields(messageMap[name]);
818
879
  for (const f of fields) checkRefs(f.type, entityNames, `message ${name}.${f.name}`);
819
880
  return { name, index, fields };
@@ -916,6 +977,10 @@ function canonicalize(s) {
916
977
  // canonical form, and therefore the hash, it had. They are in the hash when declared because
917
978
  // they are server behaviour: a client and a room that disagree about who may take a crate
918
979
  // disagree about the world.
980
+ // M7 pf3 (e): the same only-when-declared rule again, for the same reason. Server
981
+ // behaviour, so it is in the hash when present: a client and a room that disagree about who
982
+ // writes `upgrades` disagree about the world.
983
+ serverFields: c.serverFields ? [...c.serverFields] : void 0,
919
984
  perPlayer: c.perPlayer ? true : void 0,
920
985
  clientCreate: c.clientCreate ? true : void 0,
921
986
  ownership: c.ownership ? canonicalOwnership(c.ownership) : void 0
@@ -1010,11 +1075,17 @@ function validateForDeploy(schema) {
1010
1075
  message: `${c.name}: grid is only valid with visibility 'spatial-grid'`
1011
1076
  });
1012
1077
  }
1078
+ if (c.visibility === "server" && c.roles && c.roles.length > 0) {
1079
+ issues.push({
1080
+ level: "error",
1081
+ message: `${c.name}: visibility 'server' takes no roles \u2014 it is visible to no client role`
1082
+ });
1083
+ }
1013
1084
  if (c.visibility === "role") {
1014
1085
  if (!c.roles || c.roles.length === 0) {
1015
1086
  issues.push({
1016
1087
  level: "warning",
1017
- message: `${c.name}: visibility 'role' without roles \u2014 visible to no client role (likely a mistake)`
1088
+ message: `${c.name}: visibility 'role' without roles \u2014 visible to no client role (likely a mistake; say visibility: 'server' if that is what you meant)`
1018
1089
  });
1019
1090
  } else {
1020
1091
  for (const r of c.roles) {
@@ -1165,7 +1236,7 @@ function typeOf(t, at) {
1165
1236
  }
1166
1237
  function entityOptions(e, name) {
1167
1238
  const visibility = stringAt(e, "visibility");
1168
- if (visibility !== "all" && visibility !== "role" && visibility !== "spatial-grid") {
1239
+ if (visibility !== "all" && visibility !== "role" && visibility !== "spatial-grid" && visibility !== "server") {
1169
1240
  throw new Error(
1170
1241
  `schemaFromCanonical: ${name}: unknown visibility ${JSON.stringify(visibility)}`
1171
1242
  );
@@ -1179,6 +1250,11 @@ function entityOptions(e, name) {
1179
1250
  const physics = e.physics === void 0 || e.physics === null ? void 0 : physicsFromCanonical(`schemaFromCanonical: ${name}`, e.physics);
1180
1251
  const grid = e.grid === void 0 || e.grid === null ? void 0 : gridFromCanonical(name, objectAt(e, "grid"));
1181
1252
  const ownership = e.ownership === void 0 || e.ownership === null ? void 0 : ownershipFromCanonical(`schemaFromCanonical: ${name}`, e.ownership);
1253
+ const serverFields = e.serverFields === void 0 || e.serverFields === null ? void 0 : arrayAtRaw(e, "serverFields").map((f) => {
1254
+ if (typeof f !== "string")
1255
+ throw new Error(`schemaFromCanonical: ${name}: serverFields must be strings`);
1256
+ return f;
1257
+ });
1182
1258
  return {
1183
1259
  serverOwned: e.serverOwned === true,
1184
1260
  visibility,
@@ -1188,8 +1264,9 @@ function entityOptions(e, name) {
1188
1264
  // ---- M6 lane K: stateful relay ----
1189
1265
  ...e.perPlayer === true ? { perPlayer: true } : {},
1190
1266
  ...e.clientCreate === true ? { clientCreate: true } : {},
1191
- ...ownership !== void 0 ? { ownership } : {}
1267
+ ...ownership !== void 0 ? { ownership } : {},
1192
1268
  // ---- end M6 lane K ----
1269
+ ...serverFields !== void 0 ? { serverFields } : {}
1193
1270
  };
1194
1271
  }
1195
1272
  function gridFromCanonical(name, grid) {
@@ -1294,6 +1371,9 @@ var EntityCollection = class {
1294
1371
  ownerOf(id) {
1295
1372
  return this.records.get(id)?.owner;
1296
1373
  }
1374
+ reconcile(entries) {
1375
+ reconcileCollection(this, this.desc, entries);
1376
+ }
1297
1377
  get size() {
1298
1378
  return this.records.size;
1299
1379
  }
@@ -1304,6 +1384,39 @@ var EntityCollection = class {
1304
1384
  for (const [id, r] of this.records) yield [id, r.value];
1305
1385
  }
1306
1386
  };
1387
+ function reconcileCollection(coll, desc, entries) {
1388
+ const seen = /* @__PURE__ */ new Set();
1389
+ for (const entry of entries) {
1390
+ if (!Array.isArray(entry) || entry.length < 2) {
1391
+ throw new Error(
1392
+ `${desc.name}.reconcile: entries must yield [id, values] pairs, got ${JSON.stringify(entry)}`
1393
+ );
1394
+ }
1395
+ const [id, values] = entry;
1396
+ seen.add(id);
1397
+ const current = coll.get(id);
1398
+ if (current === void 0) {
1399
+ coll.add(id, values);
1400
+ continue;
1401
+ }
1402
+ assignFields(desc, current, values);
1403
+ }
1404
+ for (const id of [...coll.ids()]) if (!seen.has(id)) coll.remove(id);
1405
+ }
1406
+ function assignFields(desc, target, values) {
1407
+ if (typeof values !== "object" || values === null) {
1408
+ throw new Error(`${desc.name}: values must be an object`);
1409
+ }
1410
+ for (const [name, raw] of Object.entries(values)) {
1411
+ const index = desc.fieldIndex.get(name);
1412
+ if (index === void 0) {
1413
+ throw new Error(`${desc.name}: unknown field ${JSON.stringify(name)}`);
1414
+ }
1415
+ const field = desc.fields[index];
1416
+ const next = raw === void 0 && field.type.opt ? void 0 : normalizeValue(field.type, raw, `${desc.name}.${name}`);
1417
+ target[name] = next;
1418
+ }
1419
+ }
1307
1420
  function normalizeRecord(desc, values) {
1308
1421
  if (typeof values !== "object" || values === null) {
1309
1422
  throw new Error(`${desc.name}: values must be an object`);
@@ -1543,6 +1656,13 @@ var ByteWriter = class {
1543
1656
  return this.buf.slice(0, this.pos);
1544
1657
  }
1545
1658
  };
1659
+ function utf82(bytes) {
1660
+ try {
1661
+ return decoder.decode(bytes);
1662
+ } catch {
1663
+ throw new Error(`invalid UTF-8 in string (${bytes.length} bytes)`);
1664
+ }
1665
+ }
1546
1666
  var ByteReader = class {
1547
1667
  constructor(buf, offset = 0) {
1548
1668
  this.buf = buf;
@@ -1633,10 +1753,10 @@ var ByteReader = class {
1633
1753
  * decoded by every receiver).
1634
1754
  */
1635
1755
  str(max) {
1636
- if (max === void 0) return decoder.decode(this.blob());
1756
+ if (max === void 0) return utf82(this.blob());
1637
1757
  const n = this.varint();
1638
1758
  if (n > max) throw new Error(`string exceeds ${max} UTF-8 bytes (${n})`);
1639
- return decoder.decode(this.bytes(n));
1759
+ return utf82(this.bytes(n));
1640
1760
  }
1641
1761
  /** The rest of the buffer (a view). */
1642
1762
  rest() {
@@ -1739,13 +1859,13 @@ function writeValue(w, desc, v, path = "value") {
1739
1859
  return;
1740
1860
  }
1741
1861
  case "f32":
1862
+ case "f64": {
1742
1863
  if (typeof value !== "number") fail(path, "must be a number");
1743
- w.f32(value);
1744
- return;
1745
- case "f64":
1746
- if (typeof value !== "number") fail(path, "must be a number");
1747
- w.f64(value);
1864
+ if (!Number.isFinite(value)) fail(path, `must be a finite number (${desc.kind})`);
1865
+ if (desc.kind === "f32") w.f32(value);
1866
+ else w.f64(value);
1748
1867
  return;
1868
+ }
1749
1869
  case "str":
1750
1870
  case "ref": {
1751
1871
  if (typeof value !== "string") fail(path, "must be a string");
@@ -1779,7 +1899,12 @@ function writeValue(w, desc, v, path = "value") {
1779
1899
  }
1780
1900
  }
1781
1901
  }
1782
- function readValue(r, desc) {
1902
+ function warnNonFinite(path, value) {
1903
+ console.warn(
1904
+ `irtio: snapshot field ${path} held ${value}; restored as the field's default. The value predates the finiteness checks on write and encode.`
1905
+ );
1906
+ }
1907
+ function readValue(r, desc, opts, path = "value") {
1783
1908
  if (desc.opt && r.u8() === 0) return void 0;
1784
1909
  switch (desc.kind) {
1785
1910
  case "bool":
@@ -1793,9 +1918,15 @@ function readValue(r, desc) {
1793
1918
  case "i32":
1794
1919
  return r.i32();
1795
1920
  case "f32":
1796
- return r.f32();
1797
- case "f64":
1798
- return r.f64();
1921
+ case "f64": {
1922
+ const v = desc.kind === "f32" ? r.f32() : r.f64();
1923
+ if (!Number.isFinite(v)) {
1924
+ if (!opts?.onNonFinite) throw new Error(`must be a finite number (${desc.kind})`);
1925
+ opts.onNonFinite(path, v);
1926
+ return defaultValue(desc);
1927
+ }
1928
+ return v;
1929
+ }
1799
1930
  case "str":
1800
1931
  case "ref":
1801
1932
  return r.str(desc.kind === "str" ? desc.max : ID_MAX_BYTES);
@@ -1810,12 +1941,13 @@ function readValue(r, desc) {
1810
1941
  const n = r.varint();
1811
1942
  if (n > desc.max) throw new Error(`list exceeds max length ${desc.max} (${n})`);
1812
1943
  const out = [];
1813
- for (let i = 0; i < n; i++) out.push(readValue(r, desc.item));
1944
+ for (let i = 0; i < n; i++) out.push(readValue(r, desc.item, opts, `${path}[${i}]`));
1814
1945
  return out;
1815
1946
  }
1816
1947
  case "struct": {
1817
1948
  const o = {};
1818
- for (const [k, fd] of Object.entries(desc.fields)) o[k] = readValue(r, fd);
1949
+ for (const [k, fd] of Object.entries(desc.fields))
1950
+ o[k] = readValue(r, fd, opts, `${path}.${k}`);
1819
1951
  return o;
1820
1952
  }
1821
1953
  }
@@ -1825,9 +1957,10 @@ function writeRecord(w, fields, value, path) {
1825
1957
  fail(path, "must be an object");
1826
1958
  for (const f of fields) writeValue(w, f.type, value[f.name], `${path}.${f.name}`);
1827
1959
  }
1828
- function readRecord(r, fields) {
1960
+ function readRecord(r, fields, opts, path = "") {
1829
1961
  const o = {};
1830
- for (const f of fields) o[f.name] = readValue(r, f.type);
1962
+ for (const f of fields)
1963
+ o[f.name] = readValue(r, f.type, opts, path ? `${path}.${f.name}` : f.name);
1831
1964
  return o;
1832
1965
  }
1833
1966
  function encodeFields(fields, value) {
@@ -1887,8 +2020,9 @@ function encodeSnapshot(schema, state, header, options) {
1887
2020
  writeSnapshot(w, schema, state, header.tick, options);
1888
2021
  return w.finish();
1889
2022
  }
1890
- function decodeSnapshot(schema, bytes) {
2023
+ function decodeSnapshot(schema, bytes, options = {}) {
1891
2024
  const r = new ByteReader(bytes);
2025
+ const opts = { onNonFinite: options.onNonFinite ?? warnNonFinite };
1892
2026
  const tick = r.u32();
1893
2027
  const hash8 = r.bytes(8).slice();
1894
2028
  const state = createState(schema);
@@ -1897,13 +2031,13 @@ function decodeSnapshot(schema, bytes) {
1897
2031
  const coll = entityOf(state, c.name);
1898
2032
  const n = r.varint();
1899
2033
  for (let i = 0; i < n; i++) {
1900
- const id = r.str();
1901
- const owner = r.str();
1902
- coll.add(id, readRecord(r, c.fields), { owner });
2034
+ const id = r.str(ID_MAX_BYTES);
2035
+ const owner = r.str(ID_MAX_BYTES);
2036
+ coll.add(id, readRecord(r, c.fields, opts, `${c.name}.${id}`), { owner });
1903
2037
  }
1904
2038
  } else {
1905
2039
  const target = singletonOf(state, c.name);
1906
- const value = readRecord(r, c.fields);
2040
+ const value = readRecord(r, c.fields, opts, c.name);
1907
2041
  for (const f of c.fields) target[f.name] = value[f.name];
1908
2042
  }
1909
2043
  }
@@ -2242,6 +2376,7 @@ function skipValue(r, desc) {
2242
2376
  return;
2243
2377
  case "list": {
2244
2378
  const n = r.varint();
2379
+ if (n > desc.max) throw new Error(`list exceeds max length ${desc.max} (${n})`);
2245
2380
  for (let i = 0; i < n; i++) skipValue(r, desc.item);
2246
2381
  return;
2247
2382
  }
@@ -2389,6 +2524,11 @@ function objHandler(node) {
2389
2524
  get(target, prop, receiver) {
2390
2525
  if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
2391
2526
  const f = node.fields.get(prop);
2527
+ if (!f && prop === "reconcile") {
2528
+ throw new Error(
2529
+ `${node.label}.reconcile: reconcile() diffs the ids of an entity collection; ${node.label} is a single record. Assign its fields directly.`
2530
+ );
2531
+ }
2392
2532
  if (!f) return Reflect.get(target, prop, receiver);
2393
2533
  const v = target[f.name];
2394
2534
  if (v === null || typeof v !== "object") return v;
@@ -2635,6 +2775,14 @@ var TrackedCollection = class {
2635
2775
  ownerOf(id) {
2636
2776
  return this.inner.ownerOf(id);
2637
2777
  }
2778
+ /**
2779
+ * M7 pf3 (d): the tracked half of {@link reconcileCollection}. It calls this collection's own
2780
+ * `add`/`remove`/`get`, so every mark the hand-written bookkeeping would have made is made here
2781
+ * — including the record-proxy invalidation an `add` over a live id does.
2782
+ */
2783
+ reconcile(entries) {
2784
+ reconcileCollection(this, this.desc, entries);
2785
+ }
2638
2786
  get size() {
2639
2787
  return this.inner.size;
2640
2788
  }
@@ -2784,6 +2932,12 @@ function diffSchemas(oldSchema, newSchema) {
2784
2932
  );
2785
2933
  return changes;
2786
2934
  }
2935
+ function breakingIsWireOnly(changes) {
2936
+ const breaking = changes.filter((c) => c.kind === "breaking");
2937
+ return breaking.length > 0 && breaking.every(
2938
+ (c) => c.path === "rpc" || c.path.startsWith("rpc.") || c.path.startsWith("message.")
2939
+ );
2940
+ }
2787
2941
  function diffCollections(oldCollections, newCollections, changes) {
2788
2942
  const oldByName = new Map(oldCollections.map((c) => [c.name, c]));
2789
2943
  const newByName = new Map(newCollections.map((c) => [c.name, c]));
@@ -2858,6 +3012,14 @@ function diffCollections(oldCollections, newCollections, changes) {
2858
3012
  message: `${oldC.name}: spatial grid contract changed \u2014 changes per-client membership`
2859
3013
  });
2860
3014
  }
3015
+ if (!sameStringSet([...oldC.serverFields ?? []], [...newC.serverFields ?? []])) {
3016
+ changes.push({
3017
+ kind: "breaking",
3018
+ code: "entity.server_fields_changed",
3019
+ path: oldC.name,
3020
+ message: `${oldC.name}: serverFields changed from [${[...oldC.serverFields ?? []].join(", ")}] to [${[...newC.serverFields ?? []].join(", ")}] \u2014 changes which fields a client may write`
3021
+ });
3022
+ }
2861
3023
  diffFields(oldC.fields, newC.fields, oldC.name, changes);
2862
3024
  }
2863
3025
  }
@@ -2884,14 +3046,6 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
2884
3046
  });
2885
3047
  }
2886
3048
  }
2887
- if (oldRpcs.length !== newRpcs.length) {
2888
- changes.push({
2889
- kind: "breaking",
2890
- code: "rpc.builtin_reordered",
2891
- path: "rpc",
2892
- 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)`
2893
- });
2894
- }
2895
3049
  for (const oldR of oldRpcs) {
2896
3050
  const newR = newByName.get(oldR.name);
2897
3051
  if (!newR) continue;
@@ -2900,7 +3054,7 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
2900
3054
  kind: "breaking",
2901
3055
  code: "rpc.reordered",
2902
3056
  path: `rpc.${oldR.name}`,
2903
- 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)`
3057
+ message: `rpc ${oldR.name}: wire id changed (${oldR.index} -> ${newR.index}) \u2014 RPC ids are your declaration order, so moving a declaration moves an id and calls would reach the wrong handler (breaking)`
2904
3058
  });
2905
3059
  }
2906
3060
  if (oldR.direction !== newR.direction) {
@@ -2946,7 +3100,7 @@ function diffMessages(oldMessages, newMessages, changes) {
2946
3100
  kind: "breaking",
2947
3101
  code: "message.reordered",
2948
3102
  path: `message.${oldM.name}`,
2949
- 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)`
3103
+ message: `message ${oldM.name}: wire index changed (${oldM.index} -> ${newM.index}) \u2014 message indices are your declaration order, so peers would decode this one as another (breaking)`
2950
3104
  });
2951
3105
  }
2952
3106
  diffFields(oldM.fields, newM.fields, `message.${oldM.name}`, changes);
@@ -3193,15 +3347,18 @@ export {
3193
3347
  EntityCollection,
3194
3348
  ID_MAX_BYTES,
3195
3349
  MAX_MESSAGES,
3350
+ MAX_USER_RPCS,
3196
3351
  PHYSICS_BODY_CHANNELS,
3197
3352
  RESERVED_COLLECTION_NAMES,
3198
3353
  RESERVED_RPC_NAMES,
3354
+ RESERVED_RPC_PREFIX,
3199
3355
  SERVER_OWNER,
3200
3356
  SINGLETON_ID,
3201
3357
  angleFrom2d,
3202
3358
  applyChannel2d,
3203
3359
  applyDelta,
3204
3360
  bool,
3361
+ breakingIsWireOnly,
3205
3362
  bytesEqual,
3206
3363
  canonicalOwnership,
3207
3364
  canonicalPhysics,
@@ -3252,6 +3409,7 @@ export {
3252
3409
  parseHold,
3253
3410
  physicsFromCanonical,
3254
3411
  readValue,
3412
+ reconcileCollection,
3255
3413
  ref,
3256
3414
  schemaFromCanonical,
3257
3415
  server,
@@ -3266,6 +3424,7 @@ export {
3266
3424
  u16,
3267
3425
  u32,
3268
3426
  u8,
3427
+ utf8Length,
3269
3428
  validateForDeploy,
3270
3429
  validateValue,
3271
3430
  walkDelta,
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@irtio/schema",
3
- "version": "0.11.0",
3
+ "version": "3.0.0",
4
4
  "description": "irtio schema DSL, type inference, canonical hash, codec, change tracking, and schema diff",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/alex-irt/irtio.git",
9
+ "directory": "packages/schema"
10
+ },
6
11
  "publishConfig": {
7
12
  "access": "public"
8
13
  },