@irtio/protocol 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _irtio_schema from '@irtio/schema';
2
- import { ByteReader, RpcMap, ServerRpcs, AnySchema, RpcDesc, InstanceOf, Schema, SchemaDefs, SchemaRpc, SchemaRoles } from '@irtio/schema';
2
+ import { ByteReader, RpcMap, ServerRpcs, AnySchema, RpcDesc, InstanceOf, Schema, SchemaDefs, SchemaRpc, SchemaRoles, SchemaMessages } from '@irtio/schema';
3
3
 
4
4
  /**
5
5
  * Frame envelope: one type byte, then an opaque payload. `encodeFrame`/`decodeFrame` only
@@ -9,12 +9,18 @@ import { ByteReader, RpcMap, ServerRpcs, AnySchema, RpcDesc, InstanceOf, Schema,
9
9
  * Wire protocol version (u8), bumped on any breaking change to framing or payload shapes.
10
10
  *
11
11
  * 2: `WELCOME`'s tick slot carries the room's tick rate in ticks per second, where version 1
12
- * carried the interval in whole milliseconds. The slot kept its width and type, so a version 1
13
- * client would read `60` as a 60 ms interval and quietly run a four-times-too-long interpolation
14
- * delay and a wrong physics timestep. The bump turns that into `E_PROTOCOL_VERSION` at join,
15
- * which is a client bundle that needs redeploying rather than an afternoon of debugging.
12
+ * carried the interval in whole milliseconds (`b28a325`). The slot kept its width and type, so a
13
+ * version 1 client would read `60` as a 60 ms interval and quietly run a four-times-too-long
14
+ * interpolation delay and a wrong physics timestep.
15
+ *
16
+ * 3: `CALL` carries the caller's `clientTick` between `rpcId` and the params (D72, lag
17
+ * compensation). The params are rest-of-buffer, so an older server reading a version 3 `CALL`
18
+ * would take four bytes of the stamp as the head of the params and decode nonsense. Either bump
19
+ * turns the mismatch into `E_PROTOCOL_VERSION` at join, which is a client bundle that needs
20
+ * redeploying rather than an afternoon of debugging. 2 and 3 landed on separate branches the
21
+ * same day and met at the M6 wave 1 merge; both are spent.
16
22
  */
17
- declare const PROTOCOL_VERSION = 2;
23
+ declare const PROTOCOL_VERSION = 3;
18
24
  declare const FrameType: {
19
25
  readonly HELLO: 1;
20
26
  readonly WELCOME: 2;
@@ -250,6 +256,25 @@ declare const ErrorCode: {
250
256
  readonly code: 33;
251
257
  readonly message: "no capacity in region {region}; add a server or raise maxVms";
252
258
  };
259
+ /**
260
+ * M6 lane A (D68-d): an operator deleted this room, and its stored state is gone.
261
+ *
262
+ * Its own code rather than `E_KICKED` or `E_ROOM_CLOSED`, and the difference between the three is
263
+ * the reason it exists. `E_KICKED` is the room's own code deciding one player should leave, and
264
+ * a client's right answer is usually to show a message and stay in the game. `E_ROOM_CLOSED` is
265
+ * a room ending normally; rejoining makes a fresh one and nothing was lost. This is neither: the
266
+ * room's state has been deleted from outside the game, deliberately, by somebody with a rooms
267
+ * API credential. A client that reconnects gets an empty room rather than the one it was in.
268
+ * Different thing to tell a player, different thing for a client to do, different code.
269
+ *
270
+ * Fatal, which is load-bearing: `@irtio/client`'s fatal path leaves for good rather than
271
+ * reconnecting, and that is what stops a deleted room from being immediately recreated by the
272
+ * very clients that were in it. Close code {@link CLOSE_ROOM_DELETED}.
273
+ */
274
+ readonly E_ROOM_DELETED: {
275
+ readonly code: 34;
276
+ readonly message: "room {roomId} was deleted by an operator; its stored state is gone";
277
+ };
253
278
  };
254
279
  /**
255
280
  * The WebSocket close code the router uses for {@link ErrorCode.E_PLACEMENT}: RFC 6455's 1013,
@@ -266,6 +291,16 @@ declare const CLOSE_TRY_AGAIN_LATER = 1013;
266
291
  * they may not have. 4290 echoes HTTP 429 for the same reason a person would guess it does.
267
292
  */
268
293
  declare const CLOSE_EGRESS_WALL = 4290;
294
+ /**
295
+ * The WebSocket close code for {@link ErrorCode.E_ROOM_DELETED} (D68-d).
296
+ *
297
+ * In the application range beside {@link CLOSE_EGRESS_WALL}, and its own value for the same
298
+ * reason: a developer watching sockets close has to be able to tell an operator deleting a room
299
+ * from a wall, from a policy refusal, and from a crash, without a log they may not have. 4291 sits
300
+ * next to 4290 because both are "the platform stopped this on purpose", and the two are one apart
301
+ * so a grep for `429` in a close-code histogram finds both.
302
+ */
303
+ declare const CLOSE_ROOM_DELETED = 4291;
269
304
  type ErrorCodeName = keyof typeof ErrorCode;
270
305
  interface ErrorCatalogueEntry {
271
306
  readonly name: ErrorCodeName;
@@ -455,6 +490,17 @@ declare function encodeSchemaFrame(canonical: string): Uint8Array;
455
490
  interface Call {
456
491
  readonly reqId: number;
457
492
  readonly rpcId: number;
493
+ /**
494
+ * D72: the newest authoritative tick the caller had applied when it sent this `CALL`, so a room
495
+ * can answer the shot against the world the shooter was looking at (`ctx.clientTick`,
496
+ * `room.rewind`). `0` means "no stamp" — a server-to-client `CALL`, or a client that does not
497
+ * send one — and ticks start at 1, so zero can never be a real tick.
498
+ *
499
+ * It is a client-supplied number that selects which past the server answers from, and a client
500
+ * can lie about it. What a lie can reach is bounded by the room's history depth, and the room
501
+ * decides what it does with the answer; `concepts/lag-compensation.md` says both.
502
+ */
503
+ readonly clientTick?: number;
458
504
  /** Rest-of-buffer: opaque schema-codec-encoded params (not length-prefixed). */
459
505
  readonly params: Uint8Array;
460
506
  }
@@ -659,13 +705,60 @@ interface Msg {
659
705
  readonly target: MsgTarget;
660
706
  /** Rest-of-buffer: opaque application payload. */
661
707
  readonly payload: Uint8Array;
708
+ /**
709
+ * D70: present when this is a **typed** message — one of the shapes the schema declares, whose
710
+ * `payload` is `encodeFields(schema.messages[index].fields, value)` rather than opaque bytes.
711
+ *
712
+ * Absent for a raw `room.message(target, bytes)`. The two never mix: a receiver hands a typed
713
+ * frame only to typed listeners and a raw frame only to raw ones, on the client and in a room
714
+ * alike, so game code is never handed one under the other's API.
715
+ */
716
+ readonly typed?: {
717
+ readonly index: number;
718
+ };
662
719
  }
720
+ /**
721
+ * D70 typed peer messages: an **envelope**, not a sixth address.
722
+ *
723
+ * The addressing byte is the one byte the format already spends on every MSG, so a typed message
724
+ * costs a discriminator it was going to pay for plus two bytes of index — no new `FrameType`, and
725
+ * directed delivery keeps working because the envelope carries a target of its own right after
726
+ * the index. Layout:
727
+ *
728
+ * [5][index u16 LE][inner target kind][inner target fields][encoded payload]
729
+ *
730
+ * The inner target is the same vocabulary as an untyped MSG minus voice: `all`/`client`/`role`
731
+ * inbound, and the sender's `client` or `server` on the way out (the fan-out rewrites the slot,
732
+ * exactly as it does for raw bytes). `voice` is refused on both encode and decode — a typed
733
+ * envelope is authored by game code, and voice signaling must stay unrepresentable there.
734
+ *
735
+ * `isVoiceMsg` is unaffected: a typed frame's first byte is 5, never 4.
736
+ */
737
+ declare const MSG_KIND_TYPED = 5;
663
738
  /**
664
739
  * A one-byte peek: is this MSG payload voice signaling? The supervisor calls this on every MSG
665
740
  * before deciding where the frame goes, so it must not allocate or decode. An empty payload is
666
741
  * not voice (it is malformed, and the existing paths report that).
667
742
  */
668
743
  declare function isVoiceMsg(payload: Uint8Array): boolean;
744
+ /**
745
+ * D70: the same one-byte peek for a typed envelope. The supervisor uses it on the hot frame path
746
+ * for the same reason it uses `isVoiceMsg` — to decide where a frame goes without decoding it.
747
+ */
748
+ declare function isTypedMsg(payload: Uint8Array): boolean;
749
+ /**
750
+ * D70: may this typed envelope be **accepted from a client**?
751
+ *
752
+ * A three-byte peek, no allocation: an inbound typed message may address `all`, a client or a
753
+ * role, and nothing else. `server` has no meaning on the peer path (a room observes through
754
+ * `onMessage`; there is no typed server handler, by design) and `voice` must never be authorable
755
+ * by game code. Both are dropped by whoever sees the frame first, which is the supervisor above
756
+ * the relay fork and the relay host in the shared process.
757
+ *
758
+ * Returns `false` for anything too short to read, so a truncated frame is refused rather than
759
+ * assumed.
760
+ */
761
+ declare function typedMsgFromClientOk(payload: Uint8Array): boolean;
669
762
  declare function encodeMsg(m: Msg): Uint8Array;
670
763
  declare function decodeMsg(bytes: Uint8Array): Msg;
671
764
 
@@ -794,7 +887,7 @@ type PresenceRecord = InstanceOf<typeof presenceEntity>;
794
887
  */
795
888
  declare function withBuiltins<S extends AnySchema>(schema: S): Schema<SchemaDefs<S> & {
796
889
  clients: typeof presenceEntity;
797
- }, SchemaRpc<S>, SchemaRoles<S>>;
890
+ }, SchemaRpc<S>, SchemaRoles<S>, SchemaMessages<S>>;
798
891
  /**
799
892
  * The schema of a room with nothing deployed — no builder collections, just the
800
893
  * built-in presence merged by `withBuiltins`. The supervisor's relay forwarder encodes presence
@@ -803,7 +896,7 @@ declare function withBuiltins<S extends AnySchema>(schema: S): Schema<SchemaDefs
803
896
  */
804
897
  declare const relaySchema: Schema<{
805
898
  clients: typeof presenceEntity;
806
- }, {}, readonly string[]>;
899
+ }, {}, readonly string[], {}>;
807
900
  /** The `schemaHash8` a relay HELLO carries: 8 zero bytes ("no schema"). */
808
901
  declare const RELAY_HASH8: Uint8Array;
809
902
  declare function isRelayHash8(hash8: Uint8Array): boolean;
@@ -1518,4 +1611,4 @@ declare function originAllowed(origins: readonly string[], allowNoOrigin: boolea
1518
1611
  */
1519
1612
  declare function parseOriginList(raw: string | undefined): readonly string[];
1520
1613
 
1521
- export { type AttributeOptions, BUS_CHANNEL_RE, BUS_ERRORS, BUS_LIMITS, BUS_MAILBOX_PREFIX, BUS_OUTBOX, BUS_OUTBOX_PREFIX, BUS_SHARD_STARTING, type BusErrorCode, type BusMailboxEntry, type BusOutboxEntry, type BusProblem, CLOSE_EGRESS_WALL, CLOSE_TRY_AGAIN_LATER, CODE_ALPHABET, CPU_QUOTA_PCT_MAX, CPU_QUOTA_PCT_MIN, type Call, type ChurnIds, type ClientCallable, type Credential, DEFAULT_MAX_AWAKE, DEFAULT_MEMORY_MB, DEFAULT_ROOM_TYPE, EMPTY_PROFILE, ERROR_CATALOGUE, type ErrorCatalogueEntry, ErrorCode, type ErrorCodeDef, type ErrorCodeName, type ErrorPayload, type Frame, FrameType, HELLO_SCHEMA_SWAP_BIT, type Hello, MATCH_CODE_LENGTH, MAX_AWAKE_MAX, type Msg, type MsgTarget, NATIVE_RESERVE_FRACTION, PHYSICS_HEADROOM_MIB, PRESENCE_COLLECTION, PROFILE_KINDS, PROTOCOL_VERSION, type ParsedRoomId, type Ping, type Pong, type PresenceRecord, type ProfileKind, ProfileLedger, type ProfileRow, type ProfileSnapshot, RELAY_HASH8, RETENTION_MAX_MS, RETENTION_MIN_MS, RETENTION_RE, ROOMS_PER_VCPU, ROOM_CLASS_MAX_MB, ROOM_ID_RE, ROOM_MEMORY_MB_MAX, ROOM_MEMORY_MB_MIN, ROOM_TYPE_DELIMITER, ROOM_TYPE_RE, type Reply, type RoomSizeClass, type RoomTypeSizing, SUPERVISOR_BASELINE_MIB, VM_MEM_MIB_MAX, VM_MEM_MIB_MIN, type VmCpuSizing, type VmSizing, type VoiceClientMessage, type VoicePeer, type VoiceServerMessage, type VoiceTransportInfo, type Welcome, YOUNG_GEN_MB, builtinRpcs, busChannelProblem, busPayloadProblem, correctPayload, declarationsFit, decodeCall, decodeErrorPayload, decodeFrame, decodeHello, decodeMsg, decodePing, decodePong, decodeReply, decodeVoiceMessage, decodeWelcome, deltaPayload, diffProfiles, encodeCall, encodeCorrectFrame, encodeDeltaFrame, encodeErrorPayload, encodeFrame, encodeHello, encodeMsg, encodePing, encodePong, encodeReply, encodeSchemaFrame, encodeVoiceMessage, encodeWelcome, encodeWriteFrame, errorByCode, formatError, formatRoomId, isFrameType, isLocalhostOrigin, isMailboxAlarm, isOutboxAlarm, isRelayHash8, isVoiceMsg, mergeProfiles, nativeReserveOf, originAllowed, parseOriginList, parseRetention, parseRoomId, presenceEntity, readCorrectAppliedTick, readCorrectClientTick, readSchemaPayload, relaySchema, requestOwnership, roomClassFor, roomClassOfSource, roomHoursSourceFor, rpcByIdOf, rpcIdOf, rpcTable, scaleProfile, schemaPayload, topProfileRows, typeFootprintMib, vmCpuFor, vmSizeFor, withBuiltins, writePayload };
1614
+ export { type AttributeOptions, BUS_CHANNEL_RE, BUS_ERRORS, BUS_LIMITS, BUS_MAILBOX_PREFIX, BUS_OUTBOX, BUS_OUTBOX_PREFIX, BUS_SHARD_STARTING, type BusErrorCode, type BusMailboxEntry, type BusOutboxEntry, type BusProblem, CLOSE_EGRESS_WALL, CLOSE_ROOM_DELETED, CLOSE_TRY_AGAIN_LATER, CODE_ALPHABET, CPU_QUOTA_PCT_MAX, CPU_QUOTA_PCT_MIN, type Call, type ChurnIds, type ClientCallable, type Credential, DEFAULT_MAX_AWAKE, DEFAULT_MEMORY_MB, DEFAULT_ROOM_TYPE, EMPTY_PROFILE, ERROR_CATALOGUE, type ErrorCatalogueEntry, ErrorCode, type ErrorCodeDef, type ErrorCodeName, type ErrorPayload, type Frame, FrameType, HELLO_SCHEMA_SWAP_BIT, type Hello, MATCH_CODE_LENGTH, MAX_AWAKE_MAX, MSG_KIND_TYPED, type Msg, type MsgTarget, NATIVE_RESERVE_FRACTION, PHYSICS_HEADROOM_MIB, PRESENCE_COLLECTION, PROFILE_KINDS, PROTOCOL_VERSION, type ParsedRoomId, type Ping, type Pong, type PresenceRecord, type ProfileKind, ProfileLedger, type ProfileRow, type ProfileSnapshot, RELAY_HASH8, RETENTION_MAX_MS, RETENTION_MIN_MS, RETENTION_RE, ROOMS_PER_VCPU, ROOM_CLASS_MAX_MB, ROOM_ID_RE, ROOM_MEMORY_MB_MAX, ROOM_MEMORY_MB_MIN, ROOM_TYPE_DELIMITER, ROOM_TYPE_RE, type Reply, type RoomSizeClass, type RoomTypeSizing, SUPERVISOR_BASELINE_MIB, VM_MEM_MIB_MAX, VM_MEM_MIB_MIN, type VmCpuSizing, type VmSizing, type VoiceClientMessage, type VoicePeer, type VoiceServerMessage, type VoiceTransportInfo, type Welcome, YOUNG_GEN_MB, builtinRpcs, busChannelProblem, busPayloadProblem, correctPayload, declarationsFit, decodeCall, decodeErrorPayload, decodeFrame, decodeHello, decodeMsg, decodePing, decodePong, decodeReply, decodeVoiceMessage, decodeWelcome, deltaPayload, diffProfiles, encodeCall, encodeCorrectFrame, encodeDeltaFrame, encodeErrorPayload, encodeFrame, encodeHello, encodeMsg, encodePing, encodePong, encodeReply, encodeSchemaFrame, encodeVoiceMessage, encodeWelcome, encodeWriteFrame, errorByCode, formatError, formatRoomId, isFrameType, isLocalhostOrigin, isMailboxAlarm, isOutboxAlarm, isRelayHash8, isTypedMsg, isVoiceMsg, mergeProfiles, nativeReserveOf, originAllowed, parseOriginList, parseRetention, parseRoomId, presenceEntity, readCorrectAppliedTick, readCorrectClientTick, readSchemaPayload, relaySchema, requestOwnership, roomClassFor, roomClassOfSource, roomHoursSourceFor, rpcByIdOf, rpcIdOf, rpcTable, scaleProfile, schemaPayload, topProfileRows, typeFootprintMib, typedMsgFromClientOk, vmCpuFor, vmSizeFor, withBuiltins, writePayload };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/frame.ts
2
- var PROTOCOL_VERSION = 2;
2
+ var PROTOCOL_VERSION = 3;
3
3
  var FrameType = {
4
4
  HELLO: 1,
5
5
  WELCOME: 2,
@@ -148,10 +148,32 @@ var ErrorCode = {
148
148
  E_PLACEMENT: {
149
149
  code: 33,
150
150
  message: "no capacity in region {region}; add a server or raise maxVms"
151
+ },
152
+ // ---- M6 lane A: rooms API ----
153
+ /**
154
+ * M6 lane A (D68-d): an operator deleted this room, and its stored state is gone.
155
+ *
156
+ * Its own code rather than `E_KICKED` or `E_ROOM_CLOSED`, and the difference between the three is
157
+ * the reason it exists. `E_KICKED` is the room's own code deciding one player should leave, and
158
+ * a client's right answer is usually to show a message and stay in the game. `E_ROOM_CLOSED` is
159
+ * a room ending normally; rejoining makes a fresh one and nothing was lost. This is neither: the
160
+ * room's state has been deleted from outside the game, deliberately, by somebody with a rooms
161
+ * API credential. A client that reconnects gets an empty room rather than the one it was in.
162
+ * Different thing to tell a player, different thing for a client to do, different code.
163
+ *
164
+ * Fatal, which is load-bearing: `@irtio/client`'s fatal path leaves for good rather than
165
+ * reconnecting, and that is what stops a deleted room from being immediately recreated by the
166
+ * very clients that were in it. Close code {@link CLOSE_ROOM_DELETED}.
167
+ */
168
+ E_ROOM_DELETED: {
169
+ code: 34,
170
+ message: "room {roomId} was deleted by an operator; its stored state is gone"
151
171
  }
172
+ // ---- end M6 lane A ----
152
173
  };
153
174
  var CLOSE_TRY_AGAIN_LATER = 1013;
154
175
  var CLOSE_EGRESS_WALL = 4290;
176
+ var CLOSE_ROOM_DELETED = 4291;
155
177
  var ERROR_CATALOGUE = Object.keys(ErrorCode).map((name) => ({ name, code: ErrorCode[name].code, message: ErrorCode[name].message }));
156
178
  var BY_CODE = new Map(
157
179
  ERROR_CATALOGUE.map((e) => [e.code, e])
@@ -347,6 +369,7 @@ function encodeCall(c) {
347
369
  const w = new ByteWriter2();
348
370
  w.u32(c.reqId);
349
371
  w.u16(c.rpcId);
372
+ w.u32(c.clientTick ?? 0);
350
373
  w.bytes(c.params);
351
374
  return w.finish();
352
375
  }
@@ -354,8 +377,9 @@ function decodeCall(bytes) {
354
377
  const r = new ByteReader2(bytes);
355
378
  const reqId = r.u32();
356
379
  const rpcId = r.u16();
380
+ const clientTick = r.u32();
357
381
  const params = r.rest();
358
- return { reqId, rpcId, params };
382
+ return { reqId, rpcId, clientTick, params };
359
383
  }
360
384
  function encodeReply(r) {
361
385
  const w = new ByteWriter2();
@@ -434,6 +458,11 @@ function withBuiltins(schema) {
434
458
  {
435
459
  rpc: schema.rpc,
436
460
  roles: schema.roles,
461
+ // D70: message shapes travel with the extension. The builder's hash is what HELLO carries
462
+ // and what a typed message is validated against, but the runtime reaches its schema
463
+ // through `ext` in several places, and an extension that quietly dropped `messages` would
464
+ // make a declared message decodable on one side of the room and not the other.
465
+ ...schema.messageDefs !== void 0 && Object.keys(schema.messageDefs).length > 0 ? { messages: schema.messageDefs } : {},
437
466
  ...schema.project !== void 0 ? { project: schema.project } : {},
438
467
  allowReservedNames: true
439
468
  }
@@ -828,36 +857,87 @@ var MSG_KIND_CLIENT = 1;
828
857
  var MSG_KIND_ROLE = 2;
829
858
  var MSG_KIND_SERVER = 3;
830
859
  var MSG_KIND_VOICE = 4;
860
+ var MSG_KIND_TYPED = 5;
831
861
  function isVoiceMsg(payload) {
832
862
  return payload.length > 0 && payload[0] === MSG_KIND_VOICE;
833
863
  }
834
- function encodeMsg(m) {
835
- const w = new ByteWriter3();
836
- switch (m.target.kind) {
864
+ function isTypedMsg(payload) {
865
+ return payload.length > 0 && payload[0] === MSG_KIND_TYPED;
866
+ }
867
+ function typedMsgFromClientOk(payload) {
868
+ if (payload.length < 4 || payload[0] !== MSG_KIND_TYPED) return false;
869
+ const inner = payload[3];
870
+ return inner === MSG_KIND_ALL || inner === MSG_KIND_CLIENT || inner === MSG_KIND_ROLE;
871
+ }
872
+ function writeTarget(w, target) {
873
+ switch (target.kind) {
837
874
  case "all":
838
875
  w.u8(MSG_KIND_ALL);
839
- break;
876
+ return;
840
877
  case "client":
841
878
  w.u8(MSG_KIND_CLIENT);
842
- w.str(m.target.clientId);
843
- break;
879
+ w.str(target.clientId);
880
+ return;
844
881
  case "role":
845
882
  w.u8(MSG_KIND_ROLE);
846
- w.str(m.target.role);
847
- break;
883
+ w.str(target.role);
884
+ return;
848
885
  case "server":
849
886
  w.u8(MSG_KIND_SERVER);
850
- break;
887
+ return;
851
888
  case "voice":
852
889
  w.u8(MSG_KIND_VOICE);
853
- break;
890
+ return;
854
891
  }
892
+ }
893
+ function readTarget(r) {
894
+ const kind = r.u8();
895
+ switch (kind) {
896
+ case MSG_KIND_ALL:
897
+ return { kind: "all" };
898
+ case MSG_KIND_CLIENT:
899
+ return { kind: "client", clientId: r.str() };
900
+ case MSG_KIND_ROLE:
901
+ return { kind: "role", role: r.str() };
902
+ case MSG_KIND_SERVER:
903
+ return { kind: "server" };
904
+ case MSG_KIND_VOICE:
905
+ return { kind: "voice" };
906
+ default:
907
+ throw new Error(`decodeMsg: unknown target discriminator ${kind}`);
908
+ }
909
+ }
910
+ function encodeMsg(m) {
911
+ const w = new ByteWriter3();
912
+ if (m.typed) {
913
+ if (m.target.kind === "voice") {
914
+ throw new Error("encodeMsg: a typed message cannot address voice");
915
+ }
916
+ const index = m.typed.index;
917
+ if (!Number.isInteger(index) || index < 0 || index > 65535) {
918
+ throw new Error(`encodeMsg: typed message index out of range: ${String(index)}`);
919
+ }
920
+ w.u8(MSG_KIND_TYPED);
921
+ w.u16(index);
922
+ writeTarget(w, m.target);
923
+ w.bytes(m.payload);
924
+ return w.finish();
925
+ }
926
+ writeTarget(w, m.target);
855
927
  w.bytes(m.payload);
856
928
  return w.finish();
857
929
  }
858
930
  function decodeMsg(bytes) {
859
931
  const r = new ByteReader4(bytes);
860
932
  const kind = r.u8();
933
+ if (kind === MSG_KIND_TYPED) {
934
+ const index = r.u16();
935
+ const target2 = readTarget(r);
936
+ if (target2.kind === "voice") {
937
+ throw new Error("decodeMsg: a typed message cannot address voice");
938
+ }
939
+ return { target: target2, payload: r.rest(), typed: { index } };
940
+ }
861
941
  let target;
862
942
  switch (kind) {
863
943
  case MSG_KIND_ALL:
@@ -1225,6 +1305,7 @@ export {
1225
1305
  BUS_OUTBOX_PREFIX,
1226
1306
  BUS_SHARD_STARTING,
1227
1307
  CLOSE_EGRESS_WALL,
1308
+ CLOSE_ROOM_DELETED,
1228
1309
  CLOSE_TRY_AGAIN_LATER,
1229
1310
  CODE_ALPHABET,
1230
1311
  CPU_QUOTA_PCT_MAX,
@@ -1239,6 +1320,7 @@ export {
1239
1320
  HELLO_SCHEMA_SWAP_BIT,
1240
1321
  MATCH_CODE_LENGTH,
1241
1322
  MAX_AWAKE_MAX,
1323
+ MSG_KIND_TYPED,
1242
1324
  NATIVE_RESERVE_FRACTION,
1243
1325
  PHYSICS_HEADROOM_MIB,
1244
1326
  PRESENCE_COLLECTION,
@@ -1299,6 +1381,7 @@ export {
1299
1381
  isMailboxAlarm,
1300
1382
  isOutboxAlarm,
1301
1383
  isRelayHash8,
1384
+ isTypedMsg,
1302
1385
  isVoiceMsg,
1303
1386
  mergeProfiles,
1304
1387
  nativeReserveOf,
@@ -1322,6 +1405,7 @@ export {
1322
1405
  schemaPayload,
1323
1406
  topProfileRows,
1324
1407
  typeFootprintMib,
1408
+ typedMsgFromClientOk,
1325
1409
  vmCpuFor,
1326
1410
  vmSizeFor,
1327
1411
  withBuiltins,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/protocol",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "irtio wire protocol: frames, framing, session payloads, error codes, built-in presence",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -20,7 +20,7 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "@irtio/schema": "0.6.0"
23
+ "@irtio/schema": "0.8.0"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsup",