@irtio/schema 0.5.2 → 0.7.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 +195 -9
- package/dist/index.js +339 -5
- package/package.json +1 -1
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
|
|
@@ -204,6 +244,23 @@ interface EntityOptions {
|
|
|
204
244
|
* the schema hash, and `schemaFromCanonical` does not round-trip it. Requires `physics`.
|
|
205
245
|
*/
|
|
206
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;
|
|
207
264
|
/**
|
|
208
265
|
* D22: this collection's instances are backed by rigid bodies. `body` names the fields the
|
|
209
266
|
* simulation owns (read-only everywhere else), `intents` the owner-written inputs the step
|
|
@@ -290,6 +347,8 @@ interface CollectionDesc {
|
|
|
290
347
|
readonly interpolate: boolean;
|
|
291
348
|
/** D21: clients simulate non-owned bodies ahead. Client-side only — not in the hash. */
|
|
292
349
|
readonly predicted: boolean;
|
|
350
|
+
/** D71: unpredicted instances get a kinematic proxy locally. Client-side only — not in the hash. */
|
|
351
|
+
readonly proxy: boolean;
|
|
293
352
|
/** D22 body/intent field split, compiled. `undefined` for ordinary collections. In the hash. */
|
|
294
353
|
readonly physics: PhysicsDesc | undefined;
|
|
295
354
|
}
|
|
@@ -302,24 +361,56 @@ interface RpcDesc {
|
|
|
302
361
|
/** `undefined` = void. */
|
|
303
362
|
readonly returns: readonly FieldDesc[] | undefined;
|
|
304
363
|
}
|
|
305
|
-
|
|
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> {
|
|
306
384
|
readonly rpc?: Rpc;
|
|
307
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;
|
|
308
395
|
/** The public project id, written by `irtio init`. Identity, not shape: not part of the hash. */
|
|
309
396
|
readonly project?: string;
|
|
310
397
|
/** Internal: lets `@irtio/protocol` build the runtime-extended schema with built-in collections. */
|
|
311
398
|
readonly allowReservedNames?: boolean;
|
|
312
399
|
}
|
|
313
|
-
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> {
|
|
314
401
|
readonly defs: Defs;
|
|
315
402
|
readonly rpc: Rpc;
|
|
316
403
|
readonly roles: Roles;
|
|
404
|
+
/** D70: the declared message map, as written. */
|
|
405
|
+
readonly messageDefs: Msgs;
|
|
317
406
|
readonly project: string | undefined;
|
|
318
407
|
/** Collections sorted by name; index = wire index. */
|
|
319
408
|
readonly collections: readonly CollectionDesc[];
|
|
320
409
|
readonly collection: (name: string) => CollectionDesc;
|
|
321
410
|
/** Builder RPCs sorted by name; index = wire `rpcId` (protocol appends built-ins after). */
|
|
322
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[];
|
|
323
414
|
/** Canonical JSON (sorted keys, `canonicalVersion: 1`). */
|
|
324
415
|
readonly canonical: string;
|
|
325
416
|
/** SHA-256 of the canonical form, hex. */
|
|
@@ -327,8 +418,8 @@ interface Schema<Defs extends DefMap = DefMap, Rpc extends RpcMap = RpcMap, Role
|
|
|
327
418
|
/** First 8 bytes of the hash (HELLO / snapshot / delta header). */
|
|
328
419
|
readonly hash8: Uint8Array;
|
|
329
420
|
}
|
|
330
|
-
type AnySchema = Schema<any, any, any>;
|
|
331
|
-
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>;
|
|
332
423
|
declare const CANONICAL_VERSION = 1;
|
|
333
424
|
declare function canonicalType(d: TypeDesc): Record<string, unknown>;
|
|
334
425
|
/**
|
|
@@ -445,10 +536,42 @@ type DeepWritable<T> = T extends readonly (infer U)[] ? DeepWritable<U>[] : T ex
|
|
|
445
536
|
} : T;
|
|
446
537
|
/** The writable view of an instance the client owns (the client SDK hands these out). */
|
|
447
538
|
type Owned<T> = DeepWritable<T>;
|
|
448
|
-
type SchemaDefs<S> = S extends Schema<infer D, any, any> ? D : never;
|
|
449
|
-
type SchemaRpc<S> = S extends Schema<any, infer R, any> ? R : never;
|
|
450
|
-
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;
|
|
451
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;
|
|
452
575
|
/** Plain instance type of an entity/singleton definition. */
|
|
453
576
|
type InstanceOf<D> = D extends EntityDef<infer F, any> ? InferFields<F> : D extends SingletonDef<infer F, any> ? InferFields<F> : never;
|
|
454
577
|
/** Init type (`add()` values) of an entity definition. */
|
|
@@ -662,7 +785,17 @@ declare class ByteReader {
|
|
|
662
785
|
bytes(n: number): Uint8Array;
|
|
663
786
|
/** varint length + raw bytes (a view). */
|
|
664
787
|
blob(): Uint8Array;
|
|
665
|
-
|
|
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;
|
|
666
799
|
/** The rest of the buffer (a view). */
|
|
667
800
|
rest(): Uint8Array;
|
|
668
801
|
}
|
|
@@ -774,6 +907,59 @@ declare function applyDelta(schema: AnySchema, state: PlainState, delta: Delta):
|
|
|
774
907
|
/** Field-level diff between two plain states: what would have to be sent to turn `from` into `to`. */
|
|
775
908
|
declare function computeDirty(schema: AnySchema, from: PlainState, to: PlainState): DirtySet;
|
|
776
909
|
|
|
910
|
+
/**
|
|
911
|
+
* The measurement walker: a reader's-eye pass over *encoded* delta and snapshot bytes that
|
|
912
|
+
* reports where each byte went, keyed by collection and field.
|
|
913
|
+
*
|
|
914
|
+
* This is deliberately a second reader of the wire format rather than an instrumented writer.
|
|
915
|
+
* The encoders and `ValueSink` are untouched, so the hot path stays byte-identical and
|
|
916
|
+
* cost-identical whether or not anything is profiling; and because the walker reads exactly what
|
|
917
|
+
* `decodeDeltaFrom`/`decodeSnapshot` read, it works on the server after an encode and on the
|
|
918
|
+
* client before a decode, from the same code.
|
|
919
|
+
*
|
|
920
|
+
* The copy is kept honest by conservation: the spans it reports partition the bytes it consumed,
|
|
921
|
+
* with nothing counted twice and nothing left over (`walk.test.ts` proves it against
|
|
922
|
+
* `encodeDelta(...).length` over random schemas). If the format changes here without changing
|
|
923
|
+
* `codec.ts`, or the other way round, that property test fails.
|
|
924
|
+
*
|
|
925
|
+
* Nothing here allocates a decoded value: it skips values by width, so a profiled room pays for
|
|
926
|
+
* a second pass over the bytes and not for a second decode.
|
|
927
|
+
*/
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Where the bytes went. Every callback reports a byte count, and for one walk the counts sum to
|
|
931
|
+
* the number of bytes the walk consumed.
|
|
932
|
+
*/
|
|
933
|
+
interface WalkEvents {
|
|
934
|
+
/**
|
|
935
|
+
* Framing that belongs to no single field: `tick`, `hash8`, the dirty-collection count, and
|
|
936
|
+
* each collection's index and op count (snapshot: the header and each entity count).
|
|
937
|
+
*/
|
|
938
|
+
header(bytes: number): void;
|
|
939
|
+
/**
|
|
940
|
+
* One op's own framing: the tag byte, the id string, the owner string (add), the dirty mask
|
|
941
|
+
* and the owner string when the owner bit is set (update). Always called before that op's
|
|
942
|
+
* fields, so a consumer can route a whole op somewhere else by latching on this call.
|
|
943
|
+
*
|
|
944
|
+
* A snapshot's records report as `add` ops whose framing is the id and owner strings; a
|
|
945
|
+
* snapshot singleton has no op framing at all, only fields.
|
|
946
|
+
*/
|
|
947
|
+
op(collection: CollectionDesc, kind: 'add' | 'update' | 'remove', id: string, bytes: number): void;
|
|
948
|
+
/** One field value, nested struct masks and presence bytes included. */
|
|
949
|
+
field(collection: CollectionDesc, field: FieldDesc, bytes: number): void;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Walks one delta body, reporting spans through `events`. Accepts raw bytes or a positioned
|
|
953
|
+
* reader, which it leaves at the first byte after the body exactly as `decodeDeltaFrom` does
|
|
954
|
+
* (the `CORRECT` frame's tick suffix is the caller's). Returns the number of bytes consumed.
|
|
955
|
+
*/
|
|
956
|
+
declare function walkDelta(schema: AnySchema, bytes: Uint8Array | ByteReader, events: WalkEvents): number;
|
|
957
|
+
/**
|
|
958
|
+
* Walks one snapshot body, reporting spans through `events`. Entity records report as `add` ops;
|
|
959
|
+
* a singleton reports only its fields. Returns the number of bytes consumed.
|
|
960
|
+
*/
|
|
961
|
+
declare function walkSnapshot(schema: AnySchema, bytes: Uint8Array | ByteReader, events: WalkEvents): number;
|
|
962
|
+
|
|
777
963
|
/**
|
|
778
964
|
* `track()` — a proxy tree over a plain state that records every write into a `DirtySet`.
|
|
779
965
|
*
|
|
@@ -824,4 +1010,4 @@ interface SchemaChange {
|
|
|
824
1010
|
/** Compares two compiled schemas and returns every classified change, sorted by path then code. */
|
|
825
1011
|
declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
|
|
826
1012
|
|
|
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 };
|
|
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
|
@@ -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([
|
|
@@ -492,6 +542,7 @@ function makeRpc(direction, spec) {
|
|
|
492
542
|
}
|
|
493
543
|
var RESERVED_RPC_NAMES = ["requestOwnership"];
|
|
494
544
|
var RESERVED_COLLECTION_NAMES = ["clients"];
|
|
545
|
+
var MAX_MESSAGES = 256;
|
|
495
546
|
function defineSchema(defs, options = {}) {
|
|
496
547
|
const names = Object.keys(defs);
|
|
497
548
|
for (const n of names) {
|
|
@@ -509,6 +560,11 @@ function defineSchema(defs, options = {}) {
|
|
|
509
560
|
`${name}: predicted: true needs physics \u2014 only body-backed collections can be simulated ahead`
|
|
510
561
|
);
|
|
511
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
|
+
}
|
|
512
568
|
return {
|
|
513
569
|
name,
|
|
514
570
|
index,
|
|
@@ -527,6 +583,7 @@ function defineSchema(defs, options = {}) {
|
|
|
527
583
|
} : void 0,
|
|
528
584
|
interpolate: o.interpolate !== false,
|
|
529
585
|
predicted: o.predicted === true,
|
|
586
|
+
proxy: o.proxy !== false,
|
|
530
587
|
physics: o.physics ? compilePhysics(name, def.kind, fields, o.physics) : void 0
|
|
531
588
|
};
|
|
532
589
|
});
|
|
@@ -558,13 +615,46 @@ function defineSchema(defs, options = {}) {
|
|
|
558
615
|
});
|
|
559
616
|
const roles = options.roles ?? [];
|
|
560
617
|
if (new Set(roles).size !== roles.length) throw new Error("roles must be unique");
|
|
561
|
-
const
|
|
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 });
|
|
562
651
|
const hashBytes = sha256(new TextEncoder().encode(canonical));
|
|
563
652
|
const byName = new Map(collections.map((c) => [c.name, c]));
|
|
564
653
|
return {
|
|
565
654
|
defs,
|
|
566
655
|
rpc: rpcMap,
|
|
567
656
|
roles,
|
|
657
|
+
messageDefs: messageMap,
|
|
568
658
|
project: options.project,
|
|
569
659
|
collections,
|
|
570
660
|
collection: (name) => {
|
|
@@ -573,6 +663,7 @@ function defineSchema(defs, options = {}) {
|
|
|
573
663
|
return c;
|
|
574
664
|
},
|
|
575
665
|
rpcs,
|
|
666
|
+
messages,
|
|
576
667
|
canonical,
|
|
577
668
|
hash: toHex(hashBytes),
|
|
578
669
|
hash8: hashBytes.slice(0, 8)
|
|
@@ -648,6 +739,10 @@ function canonicalize(s) {
|
|
|
648
739
|
// Emitted only when declared: `stableStringify` drops `undefined`, so every schema written
|
|
649
740
|
// before D22 keeps the byte-identical canonical form — and therefore the hash — it had.
|
|
650
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.
|
|
651
746
|
})),
|
|
652
747
|
rpcs: s.rpcs.map((r) => ({
|
|
653
748
|
name: r.name,
|
|
@@ -655,7 +750,12 @@ function canonicalize(s) {
|
|
|
655
750
|
params: canonicalFields(r.params),
|
|
656
751
|
returns: r.returns ? canonicalFields(r.returns) : null
|
|
657
752
|
})),
|
|
658
|
-
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
|
|
659
759
|
};
|
|
660
760
|
return stableStringify(doc);
|
|
661
761
|
}
|
|
@@ -809,9 +909,17 @@ function schemaFromCanonical(json, options = {}) {
|
|
|
809
909
|
if (typeof role !== "string") throw new Error("schemaFromCanonical: roles must be strings");
|
|
810
910
|
roles.push(role);
|
|
811
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
|
+
}
|
|
812
919
|
const schema = defineSchema(defs, {
|
|
813
920
|
rpc,
|
|
814
921
|
roles,
|
|
922
|
+
...Object.keys(messages).length > 0 ? { messages } : {},
|
|
815
923
|
// A canonical form produced by `withBuiltins` names the reserved `clients` collection; a
|
|
816
924
|
// reader must be able to replay whatever it is handed.
|
|
817
925
|
allowReservedNames: true,
|
|
@@ -1327,8 +1435,21 @@ var ByteReader = class {
|
|
|
1327
1435
|
blob() {
|
|
1328
1436
|
return this.bytes(this.varint());
|
|
1329
1437
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
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));
|
|
1332
1453
|
}
|
|
1333
1454
|
/** The rest of the buffer (a view). */
|
|
1334
1455
|
rest() {
|
|
@@ -1490,7 +1611,7 @@ function readValue(r, desc) {
|
|
|
1490
1611
|
return r.f64();
|
|
1491
1612
|
case "str":
|
|
1492
1613
|
case "ref":
|
|
1493
|
-
return r.str();
|
|
1614
|
+
return r.str(desc.kind === "str" ? desc.max : ID_MAX_BYTES);
|
|
1494
1615
|
case "enum": {
|
|
1495
1616
|
const i = r.u8();
|
|
1496
1617
|
const v = desc.values[i];
|
|
@@ -1901,6 +2022,147 @@ function computeDirty(schema, from, to) {
|
|
|
1901
2022
|
return dirty;
|
|
1902
2023
|
}
|
|
1903
2024
|
|
|
2025
|
+
// src/walk.ts
|
|
2026
|
+
function skip(r, n) {
|
|
2027
|
+
if (n < 0 || r.remaining < n) throw new Error("unexpected end of buffer");
|
|
2028
|
+
r.pos += n;
|
|
2029
|
+
}
|
|
2030
|
+
function structNames2(d) {
|
|
2031
|
+
return Object.keys(d.fields);
|
|
2032
|
+
}
|
|
2033
|
+
function skipValue(r, desc) {
|
|
2034
|
+
if (desc.opt && r.u8() === 0) return;
|
|
2035
|
+
switch (desc.kind) {
|
|
2036
|
+
case "bool":
|
|
2037
|
+
case "u8":
|
|
2038
|
+
case "enum":
|
|
2039
|
+
skip(r, 1);
|
|
2040
|
+
return;
|
|
2041
|
+
case "u16":
|
|
2042
|
+
skip(r, 2);
|
|
2043
|
+
return;
|
|
2044
|
+
case "u32":
|
|
2045
|
+
case "i32":
|
|
2046
|
+
case "f32":
|
|
2047
|
+
skip(r, 4);
|
|
2048
|
+
return;
|
|
2049
|
+
case "f64":
|
|
2050
|
+
skip(r, 8);
|
|
2051
|
+
return;
|
|
2052
|
+
case "str":
|
|
2053
|
+
case "ref":
|
|
2054
|
+
skip(r, r.varint());
|
|
2055
|
+
return;
|
|
2056
|
+
case "list": {
|
|
2057
|
+
const n = r.varint();
|
|
2058
|
+
for (let i = 0; i < n; i++) skipValue(r, desc.item);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
case "struct": {
|
|
2062
|
+
for (const fd of Object.values(desc.fields)) skipValue(r, fd);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function readBit2(mask, i) {
|
|
2068
|
+
return ((mask[i >> 3] ?? 0) & 1 << (i & 7)) !== 0;
|
|
2069
|
+
}
|
|
2070
|
+
function skipDirtyField(r, desc) {
|
|
2071
|
+
if (desc.kind !== "struct") {
|
|
2072
|
+
skipValue(r, desc);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (desc.opt && r.u8() === 0) return;
|
|
2076
|
+
const names = structNames2(desc);
|
|
2077
|
+
const maskBytes = r.bytes(names.length + 7 >> 3);
|
|
2078
|
+
for (let i = 0; i < names.length; i++) {
|
|
2079
|
+
if (!readBit2(maskBytes, i)) continue;
|
|
2080
|
+
skipDirtyField(r, desc.fields[names[i]]);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
function readerFor(bytes) {
|
|
2084
|
+
return bytes instanceof ByteReader ? bytes : new ByteReader(bytes);
|
|
2085
|
+
}
|
|
2086
|
+
function walkDelta(schema, bytes, events) {
|
|
2087
|
+
const r = readerFor(bytes);
|
|
2088
|
+
const from = r.pos;
|
|
2089
|
+
let at = r.pos;
|
|
2090
|
+
skip(r, 12);
|
|
2091
|
+
const count = r.varint();
|
|
2092
|
+
events.header(r.pos - at);
|
|
2093
|
+
for (let ci = 0; ci < count; ci++) {
|
|
2094
|
+
at = r.pos;
|
|
2095
|
+
const index = r.varint();
|
|
2096
|
+
const c = schema.collections[index];
|
|
2097
|
+
if (!c) throw new Error(`delta references unknown collection index ${index}`);
|
|
2098
|
+
const opCount = r.varint();
|
|
2099
|
+
events.header(r.pos - at);
|
|
2100
|
+
for (let oi = 0; oi < opCount; oi++) {
|
|
2101
|
+
at = r.pos;
|
|
2102
|
+
const tag = r.u8();
|
|
2103
|
+
const id = r.str();
|
|
2104
|
+
if (tag === 2) {
|
|
2105
|
+
events.op(c, "remove", id, r.pos - at);
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
if (tag === 0) {
|
|
2109
|
+
skip(r, r.varint());
|
|
2110
|
+
events.op(c, "add", id, r.pos - at);
|
|
2111
|
+
for (const f of c.fields) {
|
|
2112
|
+
const start = r.pos;
|
|
2113
|
+
skipValue(r, f.type);
|
|
2114
|
+
events.field(c, f, r.pos - start);
|
|
2115
|
+
}
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
if (tag !== 1) throw new Error(`unknown delta op byte ${tag} in ${c.name}`);
|
|
2119
|
+
const n = c.fields.length;
|
|
2120
|
+
const maskBytes = r.bytes(n + 1 + 7 >> 3).slice();
|
|
2121
|
+
if (readBit2(maskBytes, n)) skip(r, r.varint());
|
|
2122
|
+
events.op(c, "update", id, r.pos - at);
|
|
2123
|
+
for (let i = 0; i < n; i++) {
|
|
2124
|
+
if (!readBit2(maskBytes, i)) continue;
|
|
2125
|
+
const f = c.fields[i];
|
|
2126
|
+
const start = r.pos;
|
|
2127
|
+
skipDirtyField(r, f.type);
|
|
2128
|
+
events.field(c, f, r.pos - start);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return r.pos - from;
|
|
2133
|
+
}
|
|
2134
|
+
function walkSnapshot(schema, bytes, events) {
|
|
2135
|
+
const r = readerFor(bytes);
|
|
2136
|
+
const from = r.pos;
|
|
2137
|
+
skip(r, 12);
|
|
2138
|
+
events.header(12);
|
|
2139
|
+
for (const c of schema.collections) {
|
|
2140
|
+
if (c.kind === "entity") {
|
|
2141
|
+
let at = r.pos;
|
|
2142
|
+
const n = r.varint();
|
|
2143
|
+
events.header(r.pos - at);
|
|
2144
|
+
for (let i = 0; i < n; i++) {
|
|
2145
|
+
at = r.pos;
|
|
2146
|
+
const id = r.str();
|
|
2147
|
+
skip(r, r.varint());
|
|
2148
|
+
events.op(c, "add", id, r.pos - at);
|
|
2149
|
+
for (const f of c.fields) {
|
|
2150
|
+
const start = r.pos;
|
|
2151
|
+
skipValue(r, f.type);
|
|
2152
|
+
events.field(c, f, r.pos - start);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
} else {
|
|
2156
|
+
for (const f of c.fields) {
|
|
2157
|
+
const start = r.pos;
|
|
2158
|
+
skipValue(r, f.type);
|
|
2159
|
+
events.field(c, f, r.pos - start);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return r.pos - from;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1904
2166
|
// src/track.ts
|
|
1905
2167
|
var fieldTables = /* @__PURE__ */ new WeakMap();
|
|
1906
2168
|
function structFields(d) {
|
|
@@ -2325,6 +2587,11 @@ function diffSchemas(oldSchema, newSchema) {
|
|
|
2325
2587
|
);
|
|
2326
2588
|
diffRpcs(oldSchema.rpcs, newSchema.rpcs, changes);
|
|
2327
2589
|
diffRoles(oldSchema.roles, newSchema.roles, changes);
|
|
2590
|
+
diffMessages(
|
|
2591
|
+
oldSchema.messages ?? [],
|
|
2592
|
+
newSchema.messages ?? [],
|
|
2593
|
+
changes
|
|
2594
|
+
);
|
|
2328
2595
|
changes.sort(
|
|
2329
2596
|
(a, b) => a.path === b.path ? a.code.localeCompare(b.code) : a.path.localeCompare(b.path)
|
|
2330
2597
|
);
|
|
@@ -2388,6 +2655,14 @@ function diffCollections(oldCollections, newCollections, changes) {
|
|
|
2388
2655
|
message: `${oldC.name}: visible roles changed from [${(oldC.roles ?? []).join(", ")}] to [${(newC.roles ?? []).join(", ")}] \u2014 changes who sees this collection`
|
|
2389
2656
|
});
|
|
2390
2657
|
}
|
|
2658
|
+
if (oldC.index !== newC.index) {
|
|
2659
|
+
changes.push({
|
|
2660
|
+
kind: "breaking",
|
|
2661
|
+
code: `${oldC.kind}.reordered`,
|
|
2662
|
+
path: oldC.name,
|
|
2663
|
+
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)`
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2391
2666
|
if (stableStringify(oldC.grid) !== stableStringify(newC.grid)) {
|
|
2392
2667
|
changes.push({
|
|
2393
2668
|
kind: "breaking",
|
|
@@ -2422,9 +2697,25 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
|
|
|
2422
2697
|
});
|
|
2423
2698
|
}
|
|
2424
2699
|
}
|
|
2700
|
+
if (oldRpcs.length !== newRpcs.length) {
|
|
2701
|
+
changes.push({
|
|
2702
|
+
kind: "breaking",
|
|
2703
|
+
code: "rpc.builtin_reordered",
|
|
2704
|
+
path: "rpc",
|
|
2705
|
+
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)`
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2425
2708
|
for (const oldR of oldRpcs) {
|
|
2426
2709
|
const newR = newByName.get(oldR.name);
|
|
2427
2710
|
if (!newR) continue;
|
|
2711
|
+
if (oldR.index !== newR.index) {
|
|
2712
|
+
changes.push({
|
|
2713
|
+
kind: "breaking",
|
|
2714
|
+
code: "rpc.reordered",
|
|
2715
|
+
path: `rpc.${oldR.name}`,
|
|
2716
|
+
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)`
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2428
2719
|
if (oldR.direction !== newR.direction) {
|
|
2429
2720
|
changes.push({
|
|
2430
2721
|
kind: "breaking",
|
|
@@ -2437,6 +2728,43 @@ function diffRpcs(oldRpcs, newRpcs, changes) {
|
|
|
2437
2728
|
diffFields(oldR.returns ?? [], newR.returns ?? [], `rpc.${oldR.name}.returns`, changes);
|
|
2438
2729
|
}
|
|
2439
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
|
+
}
|
|
2440
2768
|
function diffRoles(oldRoles, newRoles, changes) {
|
|
2441
2769
|
const oldSet = new Set(oldRoles);
|
|
2442
2770
|
const newSet = new Set(newRoles);
|
|
@@ -2677,16 +3005,20 @@ export {
|
|
|
2677
3005
|
CANONICAL_VERSION,
|
|
2678
3006
|
EntityCollection,
|
|
2679
3007
|
ID_MAX_BYTES,
|
|
3008
|
+
MAX_MESSAGES,
|
|
2680
3009
|
PHYSICS_BODY_CHANNELS,
|
|
2681
3010
|
RESERVED_COLLECTION_NAMES,
|
|
2682
3011
|
RESERVED_RPC_NAMES,
|
|
2683
3012
|
SERVER_OWNER,
|
|
2684
3013
|
SINGLETON_ID,
|
|
3014
|
+
angleFrom2d,
|
|
3015
|
+
applyChannel2d,
|
|
2685
3016
|
applyDelta,
|
|
2686
3017
|
bool,
|
|
2687
3018
|
bytesEqual,
|
|
2688
3019
|
canonicalPhysics,
|
|
2689
3020
|
canonicalType,
|
|
3021
|
+
channelOf2d,
|
|
2690
3022
|
client,
|
|
2691
3023
|
cloneValue,
|
|
2692
3024
|
collectionDirty,
|
|
@@ -2745,5 +3077,7 @@ export {
|
|
|
2745
3077
|
u8,
|
|
2746
3078
|
validateForDeploy,
|
|
2747
3079
|
validateValue,
|
|
3080
|
+
walkDelta,
|
|
3081
|
+
walkSnapshot,
|
|
2748
3082
|
writeValue
|
|
2749
3083
|
};
|