@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 +102 -9
- package/dist/index.js +119 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1381
|
-
|
|
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,
|