@irtio/schema 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -200,6 +200,80 @@ declare function applyChannel2d(channel: PhysicsBodyChannel, value: number, targ
200
200
  wz: number;
201
201
  }): void;
202
202
 
203
+ /**
204
+ * M6 lane K (D77): the declarative ownership policies a collection may declare.
205
+ *
206
+ * A relay room with state has no room file, so the three things a rung-1 room file exists for —
207
+ * add one record per player, remove it when they go, and decide contested handovers — have to be
208
+ * declarations instead. These are those declarations, compiled the way `physics` is compiled: a
209
+ * literal in the schema, a descriptor on the collection, and a canonical fragment that is emitted
210
+ * **only when declared**, so every schema written before this keeps the hash it had.
211
+ *
212
+ * They are server behaviour, not rendering behaviour, which is why they join the hash at all: a
213
+ * client and a room that disagree about who may take a crate disagree about the world, the way
214
+ * they would about `physics`, and not the way they would about `interpolate`.
215
+ *
216
+ * Nothing here reads state. Compilation is pure and the descriptors are plain data, so the same
217
+ * declaration means the same thing in a coded room, in the relay host, and in a test.
218
+ */
219
+
220
+ /** How a contested record changes hands. `free | closer | ask | never` (D77). */
221
+ type OwnershipTransfer = 'free' | 'closer' | 'ask' | 'never';
222
+ /** What happens to a held record when its hold runs out. */
223
+ type OwnershipLeaveAction = 'release' | 'nearest' | 'random' | 'remove';
224
+ /** The fields `closer` and `nearest` read positions from. Same shape `spatial-grid` uses. */
225
+ interface OwnershipPosition {
226
+ /** A numeric field of this collection: the object's x. */
227
+ readonly x: string;
228
+ /** A numeric field of this collection: the object's y. */
229
+ readonly y: string;
230
+ /** The entity collection holding one record per player, keyed by client id. */
231
+ readonly players: string;
232
+ }
233
+ /**
234
+ * Hold, then act. When a player's presence is removed, every record they own is held for `hold`
235
+ * (still theirs, still visible, unwritable and untakeable by anyone else); when the hold expires,
236
+ * `then` fires. `hold: 0` — the default — makes `then` fire at once.
237
+ */
238
+ interface OwnershipLeave {
239
+ /** `'5m'`, `'30s'`, `'2h'`, or milliseconds as a number. Default `0`. */
240
+ readonly hold?: string | number;
241
+ /** Default `'release'`, except on a `perPlayer` collection where it is `'remove'`. */
242
+ readonly then?: OwnershipLeaveAction;
243
+ }
244
+ interface OwnershipOptions {
245
+ /** Default `'free'`'s opposite: with no `ownership` declared at all the old rule applies. */
246
+ readonly transfer?: OwnershipTransfer;
247
+ /** `closer`'s flap guard: the requester must be nearer by at least this many world units. */
248
+ readonly by?: number;
249
+ /** Required by `transfer: 'closer'` and by `onLeave.then: 'nearest'`. */
250
+ readonly position?: OwnershipPosition;
251
+ readonly onLeave?: OwnershipLeave;
252
+ }
253
+ /** The compiled form. `holdMs` is milliseconds, always; `then` is always resolved. */
254
+ interface OwnershipDesc {
255
+ readonly transfer: OwnershipTransfer;
256
+ readonly by: number;
257
+ readonly position: OwnershipPosition | undefined;
258
+ readonly holdMs: number;
259
+ readonly then: OwnershipLeaveAction;
260
+ }
261
+ /** A hold window, in milliseconds. Accepts `'5m'`-style strings and plain millisecond numbers. */
262
+ declare function parseHold(at: string, raw: string | number | undefined): number;
263
+ /**
264
+ * Compiles one collection's `ownership` declaration, refusing the combinations that cannot mean
265
+ * anything. Cross-collection checks (does `position.players` name an entity?) happen in
266
+ * `defineSchema`, once every collection is compiled.
267
+ */
268
+ declare function compileOwnership(name: string, kind: 'entity' | 'singleton', fields: readonly FieldDesc[], options: OwnershipOptions, perPlayer: boolean): OwnershipDesc;
269
+ /**
270
+ * The canonical fragment. `hold` is emitted in milliseconds so the round trip through
271
+ * `schemaFromCanonical` is exact whichever spelling the author used.
272
+ */
273
+ declare function canonicalOwnership(o: OwnershipDesc): Record<string, unknown>;
274
+ /** Reads a canonical ownership fragment back (`schemaFromCanonical`). */
275
+ declare function ownershipFromCanonical(at: string, raw: unknown): OwnershipOptions;
276
+
203
277
  /**
204
278
  * `entity`, `singleton`, `server`, `client`, `defineSchema`, and the compiled schema
205
279
  * (ordered collection descriptors, sorted RPC table, canonical form, hash).
@@ -269,6 +343,27 @@ interface EntityOptions {
269
343
  * world. Requires `physics:` on the room config; the runtime rejects the pairing otherwise.
270
344
  */
271
345
  readonly physics?: EntityPhysics;
346
+ /**
347
+ * D77: one record per joined client, id = the client id, owned by them. The runtime adds it on
348
+ * join and applies `ownership.onLeave` when the player goes, which is what replaces a room
349
+ * file's `onJoin`/`onLeave` pair. Defaults its leave rule to `{ hold: 0, then: 'remove' }` and
350
+ * accepts no other `then`, because a per-player record cannot belong to someone else.
351
+ *
352
+ * Server behaviour, so it is part of the canonical form and the hash — unlike `predicted`.
353
+ */
354
+ readonly perPlayer?: boolean;
355
+ /**
356
+ * D77: clients may `add` records to this collection (the adder becomes the owner, with the id
357
+ * it chose) and the owner may `remove` one. Off by default, which is what every collection
358
+ * written before this is: `add` and `remove` from a client are refused by name.
359
+ */
360
+ readonly clientCreate?: boolean;
361
+ /**
362
+ * D77: how a contested record changes hands, and what happens to it when its owner leaves.
363
+ * Absent means the old rule and nothing else: `requestOwnership` grants only when the record is
364
+ * unowned, and a leaving player's records keep their (now absent) owner.
365
+ */
366
+ readonly ownership?: OwnershipOptions;
272
367
  }
273
368
  interface EntityDef<F extends Fields = Fields, O extends EntityOptions = EntityOptions> {
274
369
  readonly kind: 'entity';
@@ -351,6 +446,12 @@ interface CollectionDesc {
351
446
  readonly proxy: boolean;
352
447
  /** D22 body/intent field split, compiled. `undefined` for ordinary collections. In the hash. */
353
448
  readonly physics: PhysicsDesc | undefined;
449
+ /** D77: one record per joined client, owned by them. In the hash. */
450
+ readonly perPlayer: boolean;
451
+ /** D77: clients may add records here and their owner may remove them. In the hash. */
452
+ readonly clientCreate: boolean;
453
+ /** D77: the compiled transfer and leave rules. `undefined` when none were declared. In the hash. */
454
+ readonly ownership: OwnershipDesc | undefined;
354
455
  }
355
456
  interface RpcDesc {
356
457
  readonly name: string;
@@ -1010,4 +1111,4 @@ interface SchemaChange {
1010
1111
  /** Compares two compiled schemas and returns every classified change, sorted by path then code. */
1011
1112
  declare function diffSchemas(oldSchema: AnySchema, newSchema: AnySchema): SchemaChange[];
1012
1113
 
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 };
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 };
package/dist/index.js CHANGED
@@ -369,6 +369,141 @@ function applyChannel2d(channel, value, target) {
369
369
  }
370
370
  }
371
371
 
372
+ // src/policy.ts
373
+ var DURATION_RE = /^(\d+)(ms|s|m|h|d)$/;
374
+ var UNIT_MS = {
375
+ ms: 1,
376
+ s: 1e3,
377
+ m: 6e4,
378
+ h: 36e5,
379
+ d: 864e5
380
+ };
381
+ function parseHold(at, raw) {
382
+ if (raw === void 0) return 0;
383
+ if (typeof raw === "number") {
384
+ if (!Number.isFinite(raw) || raw < 0 || !Number.isInteger(raw)) {
385
+ throw new Error(`${at}: ownership.onLeave.hold must be a non-negative whole ms, got ${raw}`);
386
+ }
387
+ return raw;
388
+ }
389
+ const m = DURATION_RE.exec(raw);
390
+ if (!m) {
391
+ throw new Error(
392
+ `${at}: ownership.onLeave.hold must be a duration like '30s', '5m', '2h' or milliseconds, got ${JSON.stringify(raw)}`
393
+ );
394
+ }
395
+ return Number(m[1]) * UNIT_MS[m[2]];
396
+ }
397
+ var NUMERIC = /* @__PURE__ */ new Set(["u8", "u16", "u32", "i32", "f32", "f64"]);
398
+ function compileOwnership(name, kind, fields, options, perPlayer) {
399
+ const at = name;
400
+ if (kind !== "entity") {
401
+ throw new Error(`${at}: ownership is entity-only \u2014 a singleton has no per-record owner`);
402
+ }
403
+ const transfer = options.transfer ?? (perPlayer ? "never" : "free");
404
+ if (!["free", "closer", "ask", "never"].includes(transfer)) {
405
+ throw new Error(
406
+ `${at}: ownership.transfer must be 'free', 'closer', 'ask' or 'never', got ${JSON.stringify(
407
+ transfer
408
+ )}`
409
+ );
410
+ }
411
+ const then = options.onLeave?.then ?? (perPlayer ? "remove" : "release");
412
+ if (!["release", "nearest", "random", "remove"].includes(then)) {
413
+ throw new Error(
414
+ `${at}: ownership.onLeave.then must be 'release', 'nearest', 'random' or 'remove', got ` + JSON.stringify(then)
415
+ );
416
+ }
417
+ if (perPlayer && then !== "remove") {
418
+ throw new Error(
419
+ `${at}: a perPlayer collection only accepts onLeave.then: 'remove' \u2014 a per-player record cannot belong to someone else`
420
+ );
421
+ }
422
+ const holdMs = parseHold(at, options.onLeave?.hold);
423
+ const position = options.position;
424
+ if ((transfer === "closer" || then === "nearest") && position === void 0) {
425
+ throw new Error(
426
+ `${at}: ownership.position: { x, y, players } is required by transfer: 'closer' and by onLeave.then: 'nearest' \u2014 there is no other way to know who is nearer`
427
+ );
428
+ }
429
+ if (options.by !== void 0) {
430
+ if (transfer !== "closer") {
431
+ throw new Error(
432
+ `${at}: ownership.by is the 'closer' flap guard and means nothing without it`
433
+ );
434
+ }
435
+ if (!Number.isFinite(options.by) || options.by < 0) {
436
+ throw new Error(`${at}: ownership.by must be a non-negative finite number`);
437
+ }
438
+ }
439
+ if (position !== void 0) {
440
+ for (const axis of ["x", "y"]) {
441
+ const field = fields.find((f) => f.name === position[axis]);
442
+ if (!field || !NUMERIC.has(field.type.kind)) {
443
+ throw new Error(`${at}: ownership.position.${axis} must name a numeric field of ${at}`);
444
+ }
445
+ }
446
+ if (typeof position.players !== "string" || position.players === "") {
447
+ throw new Error(`${at}: ownership.position.players must name an entity collection`);
448
+ }
449
+ }
450
+ return {
451
+ transfer,
452
+ by: options.by ?? 0,
453
+ position: position ? { x: position.x, y: position.y, players: position.players } : void 0,
454
+ holdMs,
455
+ then
456
+ };
457
+ }
458
+ function canonicalOwnership(o) {
459
+ return {
460
+ transfer: o.transfer,
461
+ ...o.by !== 0 ? { by: o.by } : {},
462
+ ...o.position ? { position: { x: o.position.x, y: o.position.y, players: o.position.players } } : {},
463
+ // `then` is the owner-decided name for the leave action in the D77 spec, and this is plain
464
+ // data that is never awaited.
465
+ // biome-ignore lint/suspicious/noThenProperty: see above
466
+ onLeave: { hold: o.holdMs, then: o.then }
467
+ };
468
+ }
469
+ function ownershipFromCanonical(at, raw) {
470
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
471
+ throw new Error(`${at}: ownership must be an object`);
472
+ }
473
+ const o = raw;
474
+ const transfer = o.transfer;
475
+ if (typeof transfer !== "string") throw new Error(`${at}: ownership.transfer must be a string`);
476
+ const leaveRaw = o.onLeave;
477
+ if (typeof leaveRaw !== "object" || leaveRaw === null || Array.isArray(leaveRaw)) {
478
+ throw new Error(`${at}: ownership.onLeave must be an object`);
479
+ }
480
+ const leave = leaveRaw;
481
+ if (typeof leave.hold !== "number")
482
+ throw new Error(`${at}: ownership.onLeave.hold must be a ms number`);
483
+ if (typeof leave.then !== "string")
484
+ throw new Error(`${at}: ownership.onLeave.then must be a string`);
485
+ let position;
486
+ if (o.position !== void 0 && o.position !== null) {
487
+ const p = o.position;
488
+ if (typeof p.x !== "string" || typeof p.y !== "string" || typeof p.players !== "string") {
489
+ throw new Error(`${at}: ownership.position needs string x, y and players`);
490
+ }
491
+ position = { x: p.x, y: p.y, players: p.players };
492
+ }
493
+ const by = o.by;
494
+ if (by !== void 0 && typeof by !== "number")
495
+ throw new Error(`${at}: ownership.by must be a number`);
496
+ return {
497
+ transfer,
498
+ ...by !== void 0 ? { by } : {},
499
+ ...position !== void 0 ? { position } : {},
500
+ // `then` is the owner-decided name for the leave action in the D77 spec, and this is plain
501
+ // data that is never awaited.
502
+ // biome-ignore lint/suspicious/noThenProperty: see above
503
+ onLeave: { hold: leave.hold, then: leave.then }
504
+ };
505
+ }
506
+
372
507
  // src/sha256.ts
373
508
  var K = new Uint32Array([
374
509
  1116352408,
@@ -565,6 +700,19 @@ function defineSchema(defs, options = {}) {
565
700
  `${name}: proxy needs physics \u2014 only body-backed collections get a body in a client's local world`
566
701
  );
567
702
  }
703
+ const perPlayer = o.perPlayer === true;
704
+ if (perPlayer && def.kind !== "entity") {
705
+ throw new Error(`${name}: perPlayer is entity-only \u2014 a singleton has no per-player record`);
706
+ }
707
+ if (o.clientCreate === true && def.kind !== "entity") {
708
+ throw new Error(`${name}: clientCreate is entity-only \u2014 a singleton is never added`);
709
+ }
710
+ if ((perPlayer || o.clientCreate === true || o.ownership) && o.serverOwned === true) {
711
+ throw new Error(
712
+ `${name}: perPlayer, clientCreate and ownership all describe client ownership, which a serverOwned collection does not have`
713
+ );
714
+ }
715
+ const ownership = o.ownership || perPlayer ? compileOwnership(name, def.kind, fields, o.ownership ?? {}, perPlayer) : void 0;
568
716
  return {
569
717
  name,
570
718
  index,
@@ -572,6 +720,9 @@ function defineSchema(defs, options = {}) {
572
720
  fields,
573
721
  fieldIndex: new Map(fields.map((f) => [f.name, f.index])),
574
722
  serverOwned: o.serverOwned === true,
723
+ perPlayer,
724
+ clientCreate: o.clientCreate === true,
725
+ ownership,
575
726
  visibility: o.visibility ?? "all",
576
727
  roles: o.roles ? [...o.roles] : void 0,
577
728
  grid: o.grid ? {
@@ -591,6 +742,26 @@ function defineSchema(defs, options = {}) {
591
742
  for (const c of collections) {
592
743
  for (const f of c.fields) checkRefs(f.type, entityNames, `${c.name}.${f.name}`);
593
744
  }
745
+ for (const c of collections) {
746
+ const players = c.ownership?.position?.players;
747
+ if (players === void 0) continue;
748
+ const target = collections.find((o) => o.name === players);
749
+ if (!target || target.kind !== "entity") {
750
+ throw new Error(
751
+ `${c.name}: ownership.position.players ${JSON.stringify(players)} does not name an entity collection in this schema`
752
+ );
753
+ }
754
+ const position = c.ownership?.position;
755
+ if (!position) continue;
756
+ for (const axis of ["x", "y"]) {
757
+ const field = target.fields.find((f) => f.name === position[axis]);
758
+ if (!field || !["u8", "u16", "u32", "i32", "f32", "f64"].includes(field.type.kind)) {
759
+ throw new Error(
760
+ `${c.name}: ownership.position.${axis} must name a numeric field of BOTH ${c.name} and ${players} \u2014 the comparison is between an object and a player`
761
+ );
762
+ }
763
+ }
764
+ }
594
765
  const rpcMap = options.rpc ?? {};
595
766
  for (const n of Object.keys(rpcMap)) {
596
767
  assertFieldName(n);
@@ -738,7 +909,17 @@ function canonicalize(s) {
738
909
  } : void 0,
739
910
  // Emitted only when declared: `stableStringify` drops `undefined`, so every schema written
740
911
  // before D22 keeps the byte-identical canonical form — and therefore the hash — it had.
741
- physics: c.physics ? canonicalPhysics(c.physics) : void 0
912
+ physics: c.physics ? canonicalPhysics(c.physics) : void 0,
913
+ // ---- M6 lane K: stateful relay ----
914
+ // D77: the same only-when-declared rule as `grid`, `physics` and `messages`, for the same
915
+ // reason — every schema written before ownership policies existed keeps the byte-identical
916
+ // canonical form, and therefore the hash, it had. They are in the hash when declared because
917
+ // they are server behaviour: a client and a room that disagree about who may take a crate
918
+ // disagree about the world.
919
+ perPlayer: c.perPlayer ? true : void 0,
920
+ clientCreate: c.clientCreate ? true : void 0,
921
+ ownership: c.ownership ? canonicalOwnership(c.ownership) : void 0
922
+ // ---- end M6 lane K ----
742
923
  // Deliberately absent, and this is the list: `interpolate` (D20), `predicted` (D21) and
743
924
  // `proxy` (D71) are all decisions about what one client does with a body it was sent, not
744
925
  // about the shape of the world. A room and a client that disagree about any of them still
@@ -997,12 +1178,18 @@ function entityOptions(e, name) {
997
1178
  });
998
1179
  const physics = e.physics === void 0 || e.physics === null ? void 0 : physicsFromCanonical(`schemaFromCanonical: ${name}`, e.physics);
999
1180
  const grid = e.grid === void 0 || e.grid === null ? void 0 : gridFromCanonical(name, objectAt(e, "grid"));
1181
+ const ownership = e.ownership === void 0 || e.ownership === null ? void 0 : ownershipFromCanonical(`schemaFromCanonical: ${name}`, e.ownership);
1000
1182
  return {
1001
1183
  serverOwned: e.serverOwned === true,
1002
1184
  visibility,
1003
1185
  ...roles !== void 0 ? { roles } : {},
1004
1186
  ...grid !== void 0 ? { grid } : {},
1005
- ...physics !== void 0 ? { physics } : {}
1187
+ ...physics !== void 0 ? { physics } : {},
1188
+ // ---- M6 lane K: stateful relay ----
1189
+ ...e.perPlayer === true ? { perPlayer: true } : {},
1190
+ ...e.clientCreate === true ? { clientCreate: true } : {},
1191
+ ...ownership !== void 0 ? { ownership } : {}
1192
+ // ---- end M6 lane K ----
1006
1193
  };
1007
1194
  }
1008
1195
  function gridFromCanonical(name, grid) {
@@ -3016,12 +3203,14 @@ export {
3016
3203
  applyDelta,
3017
3204
  bool,
3018
3205
  bytesEqual,
3206
+ canonicalOwnership,
3019
3207
  canonicalPhysics,
3020
3208
  canonicalType,
3021
3209
  channelOf2d,
3022
3210
  client,
3023
3211
  cloneValue,
3024
3212
  collectionDirty,
3213
+ compileOwnership,
3025
3214
  compilePhysics,
3026
3215
  computeDirty,
3027
3216
  createDirtySet,
@@ -3059,6 +3248,8 @@ export {
3059
3248
  mergeMask,
3060
3249
  normalizeRecord,
3061
3250
  normalizeValue,
3251
+ ownershipFromCanonical,
3252
+ parseHold,
3062
3253
  physicsFromCanonical,
3063
3254
  readValue,
3064
3255
  ref,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/schema",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "irtio schema DSL, type inference, canonical hash, codec, change tracking, and schema diff",
5
5
  "license": "MIT",
6
6
  "publishConfig": {