@colyseus/core 0.17.44 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/build/MatchMaker.cjs +22 -13
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +22 -14
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Rewind.cjs +385 -0
  12. package/build/Rewind.cjs.map +7 -0
  13. package/build/Rewind.d.ts +291 -0
  14. package/build/Rewind.mjs +359 -0
  15. package/build/Rewind.mjs.map +7 -0
  16. package/build/Room.cjs +425 -149
  17. package/build/Room.cjs.map +3 -3
  18. package/build/Room.d.ts +314 -27
  19. package/build/Room.mjs +434 -152
  20. package/build/Room.mjs.map +3 -3
  21. package/build/RoomMessages.cjs +265 -0
  22. package/build/RoomMessages.cjs.map +7 -0
  23. package/build/RoomMessages.d.ts +49 -0
  24. package/build/RoomMessages.mjs +240 -0
  25. package/build/RoomMessages.mjs.map +7 -0
  26. package/build/RoomPlugin.cjs +252 -0
  27. package/build/RoomPlugin.cjs.map +7 -0
  28. package/build/RoomPlugin.d.ts +271 -0
  29. package/build/RoomPlugin.mjs +220 -0
  30. package/build/RoomPlugin.mjs.map +7 -0
  31. package/build/Server.cjs +49 -11
  32. package/build/Server.cjs.map +2 -2
  33. package/build/Server.d.ts +26 -0
  34. package/build/Server.mjs +50 -12
  35. package/build/Server.mjs.map +2 -2
  36. package/build/Transport.cjs +64 -2
  37. package/build/Transport.cjs.map +2 -2
  38. package/build/Transport.d.ts +108 -5
  39. package/build/Transport.mjs +63 -2
  40. package/build/Transport.mjs.map +2 -2
  41. package/build/errors/RoomExceptions.cjs +7 -4
  42. package/build/errors/RoomExceptions.cjs.map +2 -2
  43. package/build/errors/RoomExceptions.d.ts +15 -3
  44. package/build/errors/RoomExceptions.mjs +5 -3
  45. package/build/errors/RoomExceptions.mjs.map +2 -2
  46. package/build/index.cjs +30 -4
  47. package/build/index.cjs.map +2 -2
  48. package/build/index.d.ts +9 -5
  49. package/build/index.mjs +29 -6
  50. package/build/index.mjs.map +2 -2
  51. package/build/input/InputBuffer.cjs +536 -0
  52. package/build/input/InputBuffer.cjs.map +7 -0
  53. package/build/input/InputBuffer.d.ts +226 -0
  54. package/build/input/InputBuffer.mjs +507 -0
  55. package/build/input/InputBuffer.mjs.map +7 -0
  56. package/build/input/RoomInput.cjs +306 -0
  57. package/build/input/RoomInput.cjs.map +7 -0
  58. package/build/input/RoomInput.d.ts +91 -0
  59. package/build/input/RoomInput.mjs +287 -0
  60. package/build/input/RoomInput.mjs.map +7 -0
  61. package/build/input/types.cjs +18 -0
  62. package/build/input/types.cjs.map +7 -0
  63. package/build/input/types.d.ts +476 -0
  64. package/build/input/types.mjs +0 -0
  65. package/build/input/types.mjs.map +7 -0
  66. package/build/internal.cjs +61 -0
  67. package/build/internal.cjs.map +7 -0
  68. package/build/internal.d.ts +9 -0
  69. package/build/internal.mjs +29 -0
  70. package/build/internal.mjs.map +7 -0
  71. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  72. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  73. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  74. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  75. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  76. package/build/matchmaker/driver.cjs.map +1 -1
  77. package/build/matchmaker/driver.d.ts +12 -0
  78. package/build/matchmaker/driver.mjs.map +1 -1
  79. package/build/presence/LocalPresence.d.ts +1 -1
  80. package/build/rooms/LobbyRoom.cjs +8 -10
  81. package/build/rooms/LobbyRoom.cjs.map +2 -2
  82. package/build/rooms/LobbyRoom.d.ts +4 -3
  83. package/build/rooms/LobbyRoom.mjs +8 -10
  84. package/build/rooms/LobbyRoom.mjs.map +2 -2
  85. package/build/rooms/QueueRoom.cjs +1 -1
  86. package/build/rooms/QueueRoom.cjs.map +2 -2
  87. package/build/rooms/QueueRoom.mjs +1 -1
  88. package/build/rooms/QueueRoom.mjs.map +2 -2
  89. package/build/rooms/RelayRoom.cjs +12 -16
  90. package/build/rooms/RelayRoom.cjs.map +2 -2
  91. package/build/rooms/RelayRoom.d.ts +32 -11
  92. package/build/rooms/RelayRoom.mjs +10 -16
  93. package/build/rooms/RelayRoom.mjs.map +2 -2
  94. package/build/router/default_routes.cjs +4 -1
  95. package/build/router/default_routes.cjs.map +2 -2
  96. package/build/router/default_routes.mjs +4 -1
  97. package/build/router/default_routes.mjs.map +2 -2
  98. package/build/router/index.cjs +71 -5
  99. package/build/router/index.cjs.map +2 -2
  100. package/build/router/index.d.ts +30 -6
  101. package/build/router/index.mjs +72 -7
  102. package/build/router/index.mjs.map +3 -3
  103. package/build/serializer/NoneSerializer.cjs +2 -2
  104. package/build/serializer/NoneSerializer.cjs.map +2 -2
  105. package/build/serializer/NoneSerializer.d.ts +3 -3
  106. package/build/serializer/NoneSerializer.mjs +2 -2
  107. package/build/serializer/NoneSerializer.mjs.map +2 -2
  108. package/build/serializer/SchemaSerializer.cjs +44 -14
  109. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  110. package/build/serializer/SchemaSerializer.d.ts +3 -3
  111. package/build/serializer/SchemaSerializer.mjs +46 -16
  112. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  113. package/build/serializer/Serializer.cjs.map +1 -1
  114. package/build/serializer/Serializer.d.ts +12 -2
  115. package/build/utils/DevMode.cjs +14 -14
  116. package/build/utils/DevMode.cjs.map +2 -2
  117. package/build/utils/DevMode.d.ts +0 -2
  118. package/build/utils/DevMode.mjs +14 -12
  119. package/build/utils/DevMode.mjs.map +2 -2
  120. package/build/utils/UserSessionIndex.cjs +162 -0
  121. package/build/utils/UserSessionIndex.cjs.map +7 -0
  122. package/build/utils/UserSessionIndex.d.ts +166 -0
  123. package/build/utils/UserSessionIndex.mjs +130 -0
  124. package/build/utils/UserSessionIndex.mjs.map +7 -0
  125. package/build/utils/Utils.cjs +3 -0
  126. package/build/utils/Utils.cjs.map +2 -2
  127. package/build/utils/Utils.d.ts +8 -0
  128. package/build/utils/Utils.mjs +2 -0
  129. package/build/utils/Utils.mjs.map +2 -2
  130. package/package.json +19 -14
  131. package/src/MatchMaker.ts +47 -15
  132. package/src/Protocol.ts +130 -59
  133. package/src/Rewind.ts +572 -0
  134. package/src/Room.ts +649 -235
  135. package/src/RoomMessages.ts +342 -0
  136. package/src/RoomPlugin.ts +563 -0
  137. package/src/Server.ts +82 -15
  138. package/src/Transport.ts +172 -9
  139. package/src/errors/RoomExceptions.ts +18 -4
  140. package/src/index.ts +23 -3
  141. package/src/input/InputBuffer.ts +549 -0
  142. package/src/input/RoomInput.ts +337 -0
  143. package/src/input/types.ts +503 -0
  144. package/src/internal.ts +46 -0
  145. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  146. package/src/matchmaker/driver.ts +13 -0
  147. package/src/rooms/LobbyRoom.ts +12 -8
  148. package/src/rooms/QueueRoom.ts +1 -1
  149. package/src/rooms/RelayRoom.ts +9 -15
  150. package/src/router/default_routes.ts +6 -1
  151. package/src/router/index.ts +117 -11
  152. package/src/serializer/NoneSerializer.ts +3 -3
  153. package/src/serializer/SchemaSerializer.ts +110 -19
  154. package/src/serializer/Serializer.ts +13 -2
  155. package/src/utils/DevMode.ts +18 -13
  156. package/src/utils/UserSessionIndex.ts +311 -0
  157. package/src/utils/Utils.ts +9 -0
@@ -1,5 +1,5 @@
1
1
  // packages/core/src/Protocol.ts
2
- import { Packr } from "@colyseus/msgpackr";
2
+ import { Packr, RESERVE_START_SPACE } from "msgpackr";
3
3
  import { encode } from "@colyseus/schema";
4
4
  import { Protocol } from "@colyseus/shared-types";
5
5
  var IpcProtocol = {
@@ -9,63 +9,128 @@ var IpcProtocol = {
9
9
  };
10
10
  var packr = new Packr({
11
11
  useRecords: false
12
- // increased compatibility with decoders other than "msgpackr"
12
+ // interop with non-msgpackr decoders
13
13
  });
14
- packr.encode(void 0);
14
+ var frameBuffer = Buffer.allocUnsafe(8192);
15
+ function ensureFrameCapacity(capacity) {
16
+ if (capacity > frameBuffer.byteLength) {
17
+ const next = Buffer.allocUnsafe(capacity);
18
+ frameBuffer.copy(next);
19
+ frameBuffer = next;
20
+ }
21
+ }
22
+ var it = { offset: 0 };
15
23
  var getMessageBytes = {
16
- [Protocol.JOIN_ROOM]: (reconnectionToken, serializerId, handshake) => {
17
- const it = { offset: 1 };
18
- packr.buffer[0] = Protocol.JOIN_ROOM;
19
- packr.buffer[it.offset++] = Buffer.byteLength(reconnectionToken, "utf8");
20
- encode.utf8Write(packr.buffer, reconnectionToken, it);
21
- packr.buffer[it.offset++] = Buffer.byteLength(serializerId, "utf8");
22
- encode.utf8Write(packr.buffer, serializerId, it);
24
+ /**
25
+ * Build the JOIN_ROOM payload.
26
+ *
27
+ * Wire layout:
28
+ * [JOIN_ROOM byte][rt-len][rt][sid-len][sid][stateReflectionLen varint][stateReflection][...sections]
29
+ *
30
+ * The varint length prefix on `stateReflection` is required because the
31
+ * schema decoder on the SDK side runs `while (offset < bytes.byteLength)`
32
+ * — without a boundary it consumes past the state reflection into the
33
+ * trailing tagged-section bytes, producing "definition mismatch" warnings.
34
+ * Length is `0` when no state reflection is present.
35
+ *
36
+ * Each trailing section is `[tag (uint8)][length (varint)][payload]`. The
37
+ * SDK skips unknown tags via `length`, so new sections can be added without
38
+ * breaking older clients. See {@link HandshakeSection} in shared-types.
39
+ */
40
+ [Protocol.JOIN_ROOM]: (reconnectionToken, serializerId, handshake, extraSections) => {
41
+ const reconnectionTokenLength = Buffer.byteLength(reconnectionToken, "utf8");
42
+ const serializerIdLength = Buffer.byteLength(serializerId, "utf8");
23
43
  let handshakeLength = handshake?.byteLength || 0;
24
- if (handshakeLength > packr.buffer.byteLength - it.offset) {
25
- packr.useBuffer(Buffer.alloc(it.offset + handshakeLength, packr.buffer));
44
+ let extraLength = 0;
45
+ if (extraSections !== void 0) {
46
+ for (let i = 0; i < extraSections.length; i++) {
47
+ extraLength += 1 + 9 + extraSections[i].bytes.byteLength;
48
+ }
26
49
  }
50
+ ensureFrameCapacity(1 + 1 + reconnectionTokenLength + 1 + serializerIdLength + 9 + handshakeLength + extraLength);
51
+ it.offset = 1;
52
+ frameBuffer[0] = Protocol.JOIN_ROOM;
53
+ frameBuffer[it.offset++] = reconnectionTokenLength;
54
+ encode.utf8Write(frameBuffer, reconnectionToken, it);
55
+ frameBuffer[it.offset++] = serializerIdLength;
56
+ encode.utf8Write(frameBuffer, serializerId, it);
57
+ encode.number(frameBuffer, handshakeLength, it);
27
58
  if (handshakeLength > 0) {
28
- packr.buffer.set(handshake, it.offset);
59
+ frameBuffer.set(handshake, it.offset);
60
+ it.offset += handshakeLength;
29
61
  }
30
- return Buffer.from(packr.buffer.subarray(0, it.offset + handshakeLength));
62
+ if (extraSections !== void 0) {
63
+ for (let i = 0; i < extraSections.length; i++) {
64
+ const section = extraSections[i];
65
+ frameBuffer[it.offset++] = section.tag;
66
+ encode.number(frameBuffer, section.bytes.byteLength, it);
67
+ frameBuffer.set(section.bytes, it.offset);
68
+ it.offset += section.bytes.byteLength;
69
+ }
70
+ }
71
+ return Buffer.from(frameBuffer.subarray(0, it.offset));
31
72
  },
32
73
  [Protocol.ERROR]: (code, message = "") => {
33
- const it = { offset: 1 };
34
- packr.buffer[0] = Protocol.ERROR;
35
- encode.number(packr.buffer, code, it);
36
- encode.string(packr.buffer, message, it);
37
- return Buffer.from(packr.buffer.subarray(0, it.offset));
74
+ ensureFrameCapacity(1 + 9 + 9 + Buffer.byteLength(message, "utf8"));
75
+ it.offset = 1;
76
+ frameBuffer[0] = Protocol.ERROR;
77
+ encode.number(frameBuffer, code, it);
78
+ encode.string(frameBuffer, message, it);
79
+ return Buffer.from(frameBuffer.subarray(0, it.offset));
38
80
  },
39
81
  [Protocol.ROOM_STATE]: (bytes) => {
40
82
  return [Protocol.ROOM_STATE, ...bytes];
41
83
  },
84
+ /**
85
+ * Reply to a client {@link Protocol.ROOM_REQUEST}.
86
+ *
87
+ * Wire layout: `[ROOM_RESPONSE byte][requestId varint][status uint8][msgpack payload?]`
88
+ *
89
+ * `requestId` is opaque — echoed back exactly as the SDK sent it so the
90
+ * pending callback/promise can be correlated. `status` is a
91
+ * {@link ResponseStatus} (0 = OK, 1 = REJECTED, 2 = ERROR). The payload is
92
+ * omitted when a handler resolves with `undefined`. Returns a fresh Buffer copy
93
+ * for the same back-pressure reason documented on `raw` below.
94
+ */
95
+ [Protocol.ROOM_RESPONSE]: (requestId, status, message) => {
96
+ it.offset = 1;
97
+ frameBuffer[0] = Protocol.ROOM_RESPONSE;
98
+ encode.number(frameBuffer, requestId, it);
99
+ frameBuffer[it.offset++] = status;
100
+ const headerLength = it.offset;
101
+ if (message !== void 0) {
102
+ const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
103
+ packed.set(frameBuffer.subarray(0, headerLength), 0);
104
+ return Buffer.from(packed);
105
+ }
106
+ return Buffer.from(frameBuffer.subarray(0, headerLength));
107
+ },
42
108
  [Protocol.PING]: () => {
43
- packr.buffer[0] = Protocol.PING;
44
- return Buffer.from(packr.buffer.subarray(0, 1));
109
+ frameBuffer[0] = Protocol.PING;
110
+ return Buffer.from(frameBuffer.subarray(0, 1));
45
111
  },
112
+ // Returns a fresh copy: frameBuffer/packr are reused next call, but callers pass
113
+ // the result to async consumers (Node retains it for libuv writev across ticks).
114
+ // See benchmark/test-buffer-corruption.ts for the back-pressure repro.
46
115
  raw: (code, type, message, rawMessage) => {
47
- const it = { offset: 1 };
48
- packr.buffer[0] = code;
116
+ it.offset = 1;
117
+ frameBuffer[0] = code;
49
118
  if (typeof type === "string") {
50
- encode.string(packr.buffer, type, it);
119
+ encode.string(frameBuffer, type, it);
51
120
  } else {
52
- encode.number(packr.buffer, type, it);
121
+ encode.number(frameBuffer, type, it);
53
122
  }
123
+ const headerLength = it.offset;
54
124
  if (message !== void 0) {
55
- packr.position = 0;
56
- if (process.env.NODE_ENV !== "production") {
57
- packr.useBuffer(packr.buffer);
58
- }
59
- const endOfBufferOffset = packr.pack(message, 2048 + it.offset).byteLength;
60
- return Buffer.from(packr.buffer.subarray(0, endOfBufferOffset));
125
+ const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);
126
+ packed.set(frameBuffer.subarray(0, headerLength), 0);
127
+ return Buffer.from(packed);
61
128
  } else if (rawMessage !== void 0) {
62
- if (rawMessage.length + it.offset > packr.buffer.byteLength) {
63
- packr.useBuffer(Buffer.alloc(it.offset + rawMessage.length, packr.buffer));
64
- }
65
- packr.buffer.set(rawMessage, it.offset);
66
- return Buffer.from(packr.buffer.subarray(0, it.offset + rawMessage.byteLength));
129
+ ensureFrameCapacity(headerLength + rawMessage.byteLength);
130
+ frameBuffer.set(rawMessage, headerLength);
131
+ return Buffer.from(frameBuffer.subarray(0, headerLength + rawMessage.byteLength));
67
132
  } else {
68
- return Buffer.from(packr.buffer.subarray(0, it.offset));
133
+ return Buffer.from(frameBuffer.subarray(0, headerLength));
69
134
  }
70
135
  }
71
136
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/Protocol.ts"],
4
- "sourcesContent": ["import { Packr } from '@colyseus/msgpackr';\nimport { encode, type Iterator } from '@colyseus/schema';\nimport { Protocol } from '@colyseus/shared-types';\n\n// Inter-process communication protocol\nexport const IpcProtocol = {\n SUCCESS: 0,\n ERROR: 1,\n TIMEOUT: 2,\n} as const;\nexport type IpcProtocol = typeof IpcProtocol[keyof typeof IpcProtocol];\n\nconst packr = new Packr({\n useRecords: false, // increased compatibility with decoders other than \"msgpackr\"\n});\n\n// msgpackr workaround: initialize buffer\npackr.encode(undefined);\n\nexport const getMessageBytes = {\n [Protocol.JOIN_ROOM]: (reconnectionToken: string, serializerId: string, handshake?: Uint8Array) => {\n const it: Iterator = { offset: 1 };\n packr.buffer[0] = Protocol.JOIN_ROOM;\n\n packr.buffer[it.offset++] = Buffer.byteLength(reconnectionToken, \"utf8\");\n encode.utf8Write(packr.buffer, reconnectionToken, it);\n\n packr.buffer[it.offset++] = Buffer.byteLength(serializerId, \"utf8\");\n encode.utf8Write(packr.buffer, serializerId, it);\n\n let handshakeLength = handshake?.byteLength || 0;\n\n // check if buffer needs to be resized\n if (handshakeLength > packr.buffer.byteLength - it.offset) {\n packr.useBuffer(Buffer.alloc(it.offset + handshakeLength, packr.buffer));\n }\n\n if (handshakeLength > 0) {\n packr.buffer.set(handshake, it.offset);\n }\n\n return Buffer.from(packr.buffer.subarray(0, it.offset + handshakeLength));\n },\n\n [Protocol.ERROR]: (code: number, message: string = '') => {\n const it: Iterator = { offset: 1 };\n packr.buffer[0] = Protocol.ERROR;\n\n encode.number(packr.buffer, code, it);\n encode.string(packr.buffer, message, it);\n\n return Buffer.from(packr.buffer.subarray(0, it.offset));\n },\n\n [Protocol.ROOM_STATE]: (bytes: number[]) => {\n return [Protocol.ROOM_STATE, ...bytes];\n },\n\n [Protocol.PING]: () => {\n packr.buffer[0] = Protocol.PING;\n return Buffer.from(packr.buffer.subarray(0, 1));\n },\n\n raw: (code: Protocol, type: string | number, message?: any, rawMessage?: Uint8Array | Buffer) => {\n const it: Iterator = { offset: 1 };\n packr.buffer[0] = code;\n\n if (typeof (type) === 'string') {\n encode.string(packr.buffer, type, it);\n\n } else {\n encode.number(packr.buffer, type, it);\n }\n\n if (message !== undefined) {\n // force to encode from offset\n packr.position = 0;\n\n //\n // TODO: remove this after issue is fixed https://github.com/kriszyp/msgpackr/issues/139\n //\n // - This check is only required when running integration tests.\n // (colyseus.js' usage of msgpackr/buffer is conflicting)\n //\n if (process.env.NODE_ENV !== \"production\") {\n packr.useBuffer(packr.buffer);\n }\n\n // pack message into the same packr.buffer\n const endOfBufferOffset = packr.pack(message, 2048 + it.offset).byteLength;\n // 2048 = RESERVE_START_SPACE\n return Buffer.from(packr.buffer.subarray(0, endOfBufferOffset));\n\n } else if (rawMessage !== undefined) {\n\n // check if buffer needs to be resized\n // TODO: can we avoid this?\n if (rawMessage.length + it.offset > packr.buffer.byteLength) {\n packr.useBuffer(Buffer.alloc(it.offset + rawMessage.length, packr.buffer));\n }\n\n // copy raw message into packr.buffer\n packr.buffer.set(rawMessage, it.offset);\n\n return Buffer.from(packr.buffer.subarray(0, it.offset + rawMessage.byteLength));\n\n } else {\n return Buffer.from(packr.buffer.subarray(0, it.offset));\n }\n },\n\n};\n\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Packr, RESERVE_START_SPACE } from 'msgpackr';\nimport { encode, type Iterator } from '@colyseus/schema';\nimport { Protocol } from '@colyseus/shared-types';\n\n// Inter-process communication protocol\nexport const IpcProtocol = {\n SUCCESS: 0,\n ERROR: 1,\n TIMEOUT: 2,\n} as const;\nexport type IpcProtocol = typeof IpcProtocol[keyof typeof IpcProtocol];\n\nconst packr = new Packr({\n useRecords: false, // interop with non-msgpackr decoders\n});\n\n// Buffer for assembling outgoing frames; keeps us off msgpackr's internal\n// buffer so `core` can use the upstream package directly.\nlet frameBuffer = Buffer.allocUnsafe(8192);\n\n// Grow to `capacity`, preserving written bytes (`rawMessage` writes the header\n// before the payload size is known).\nfunction ensureFrameCapacity(capacity: number) {\n if (capacity > frameBuffer.byteLength) {\n const next = Buffer.allocUnsafe(capacity);\n frameBuffer.copy(next);\n frameBuffer = next;\n }\n}\n\n// Shared across calls to avoid a per-call allocation on the hot send path.\n// Safe: encode.* mutate `.offset` in place and never re-enter this module.\nconst it: Iterator = { offset: 0 };\n\nexport const getMessageBytes = {\n /**\n * Build the JOIN_ROOM payload.\n *\n * Wire layout:\n * [JOIN_ROOM byte][rt-len][rt][sid-len][sid][stateReflectionLen varint][stateReflection][...sections]\n *\n * The varint length prefix on `stateReflection` is required because the\n * schema decoder on the SDK side runs `while (offset < bytes.byteLength)`\n * \u2014 without a boundary it consumes past the state reflection into the\n * trailing tagged-section bytes, producing \"definition mismatch\" warnings.\n * Length is `0` when no state reflection is present.\n *\n * Each trailing section is `[tag (uint8)][length (varint)][payload]`. The\n * SDK skips unknown tags via `length`, so new sections can be added without\n * breaking older clients. See {@link HandshakeSection} in shared-types.\n */\n [Protocol.JOIN_ROOM]: (\n reconnectionToken: string,\n serializerId: string,\n handshake?: Uint8Array,\n extraSections?: Array<{ tag: number; bytes: Uint8Array }>,\n ) => {\n const reconnectionTokenLength = Buffer.byteLength(reconnectionToken, \"utf8\");\n const serializerIdLength = Buffer.byteLength(serializerId, \"utf8\");\n let handshakeLength = handshake?.byteLength || 0;\n\n // per section: 1 tag byte + 9 (max varint) + payload\n let extraLength = 0;\n if (extraSections !== undefined) {\n for (let i = 0; i < extraSections.length; i++) {\n extraLength += 1 + 9 + extraSections[i].bytes.byteLength;\n }\n }\n\n // capacity: header + 9 (handshake-len varint) + handshake + sections\n ensureFrameCapacity(1 + 1 + reconnectionTokenLength + 1 + serializerIdLength + 9 + handshakeLength + extraLength);\n\n it.offset = 1;\n frameBuffer[0] = Protocol.JOIN_ROOM;\n\n frameBuffer[it.offset++] = reconnectionTokenLength;\n encode.utf8Write(frameBuffer, reconnectionToken, it);\n\n frameBuffer[it.offset++] = serializerIdLength;\n encode.utf8Write(frameBuffer, serializerId, it);\n\n encode.number(frameBuffer, handshakeLength, it);\n if (handshakeLength > 0) {\n frameBuffer.set(handshake, it.offset);\n it.offset += handshakeLength;\n }\n\n if (extraSections !== undefined) {\n for (let i = 0; i < extraSections.length; i++) {\n const section = extraSections[i];\n frameBuffer[it.offset++] = section.tag;\n encode.number(frameBuffer, section.bytes.byteLength, it);\n frameBuffer.set(section.bytes, it.offset);\n it.offset += section.bytes.byteLength;\n }\n }\n\n return Buffer.from(frameBuffer.subarray(0, it.offset));\n },\n\n [Protocol.ERROR]: (code: number, message: string = '') => {\n // capacity: 1 + code varint + length-prefixed message\n ensureFrameCapacity(1 + 9 + 9 + Buffer.byteLength(message, \"utf8\"));\n\n it.offset = 1;\n frameBuffer[0] = Protocol.ERROR;\n\n encode.number(frameBuffer, code, it);\n encode.string(frameBuffer, message, it);\n\n return Buffer.from(frameBuffer.subarray(0, it.offset));\n },\n\n [Protocol.ROOM_STATE]: (bytes: number[]) => {\n return [Protocol.ROOM_STATE, ...bytes];\n },\n\n /**\n * Reply to a client {@link Protocol.ROOM_REQUEST}.\n *\n * Wire layout: `[ROOM_RESPONSE byte][requestId varint][status uint8][msgpack payload?]`\n *\n * `requestId` is opaque \u2014 echoed back exactly as the SDK sent it so the\n * pending callback/promise can be correlated. `status` is a\n * {@link ResponseStatus} (0 = OK, 1 = REJECTED, 2 = ERROR). The payload is\n * omitted when a handler resolves with `undefined`. Returns a fresh Buffer copy\n * for the same back-pressure reason documented on `raw` below.\n */\n [Protocol.ROOM_RESPONSE]: (requestId: number, status: number, message?: any): Buffer => {\n it.offset = 1;\n frameBuffer[0] = Protocol.ROOM_RESPONSE;\n\n encode.number(frameBuffer, requestId, it);\n frameBuffer[it.offset++] = status;\n const headerLength = it.offset;\n\n if (message !== undefined) {\n // reserve `headerLength` bytes up front in the pack output for the header\n const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);\n packed.set(frameBuffer.subarray(0, headerLength), 0);\n return Buffer.from(packed);\n }\n\n return Buffer.from(frameBuffer.subarray(0, headerLength));\n },\n\n [Protocol.PING]: () => {\n frameBuffer[0] = Protocol.PING;\n return Buffer.from(frameBuffer.subarray(0, 1));\n },\n\n // Returns a fresh copy: frameBuffer/packr are reused next call, but callers pass\n // the result to async consumers (Node retains it for libuv writev across ticks).\n // See benchmark/test-buffer-corruption.ts for the back-pressure repro.\n raw: (code: Protocol, type: string | number, message?: any, rawMessage?: Uint8Array | Buffer): Buffer => {\n it.offset = 1;\n frameBuffer[0] = code;\n\n if (typeof (type) === 'string') {\n encode.string(frameBuffer, type, it);\n\n } else {\n encode.number(frameBuffer, type, it);\n }\n const headerLength = it.offset;\n\n if (message !== undefined) {\n // reserve `headerLength` bytes up front in the pack output for the header\n const packed = packr.pack(message, RESERVE_START_SPACE | headerLength);\n packed.set(frameBuffer.subarray(0, headerLength), 0);\n return Buffer.from(packed);\n\n } else if (rawMessage !== undefined) {\n ensureFrameCapacity(headerLength + rawMessage.byteLength);\n frameBuffer.set(rawMessage, headerLength);\n return Buffer.from(frameBuffer.subarray(0, headerLength + rawMessage.byteLength));\n\n } else {\n return Buffer.from(frameBuffer.subarray(0, headerLength));\n }\n },\n\n};\n\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/core/src/Rewind.ts
21
+ var Rewind_exports = {};
22
+ __export(Rewind_exports, {
23
+ Rewind: () => Rewind,
24
+ RewindView: () => RewindView
25
+ });
26
+ module.exports = __toCommonJS(Rewind_exports);
27
+ var import_schema = require("@colyseus/schema");
28
+ var import_Utils = require("./utils/Utils.cjs");
29
+ var DEFAULT_MAX_REWIND_MS = 500;
30
+ var DEFAULT_SAMPLE_INTERVAL_MS = 1e3 / 60;
31
+ var $HISTORY = /* @__PURE__ */ Symbol("rewind.history");
32
+ function fieldIndexOf(instance, field) {
33
+ const md = instance.constructor[import_Utils.$METADATA];
34
+ const idx = md?.[field];
35
+ return typeof idx === "number" ? idx : -1;
36
+ }
37
+ var EntityHistory = class {
38
+ constructor(fields, fieldIdx, maxRewindMs, sampleIntervalMs, step, reckoned, groupId) {
39
+ // hold (vs lerp) between samples
40
+ this.head = 0;
41
+ // next write slot
42
+ this.count = 0;
43
+ this.fields = fields;
44
+ this.fieldIdx = fieldIdx;
45
+ this.step = step;
46
+ this.reckoned = reckoned;
47
+ this.groupId = groupId;
48
+ this.cap = Math.max(2, Math.ceil(maxRewindMs / sampleIntervalMs) + 4);
49
+ this.t = new Float64Array(this.cap);
50
+ this.cols = fields.map(() => new Float64Array(this.cap));
51
+ }
52
+ /** `values` = the entity's dense `$values` array. Direct array reads — no
53
+ * per-field megamorphic accessor. */
54
+ record(time, values) {
55
+ const fieldIdx = this.fieldIdx;
56
+ this.t[this.head] = time;
57
+ for (let f = 0; f < fieldIdx.length; f++) this.cols[f][this.head] = values[fieldIdx[f]];
58
+ this.head = (this.head + 1) % this.cap;
59
+ if (this.count < this.cap) this.count++;
60
+ }
61
+ /** Interpolated value of column `col` at `time`, clamped to the retained range. */
62
+ valueAt(time, col) {
63
+ const c = this.cols[col];
64
+ const oldest = (this.head - this.count + this.cap) % this.cap;
65
+ if (time <= this.t[oldest]) return c[oldest];
66
+ const newest = (this.head - 1 + this.cap) % this.cap;
67
+ if (time >= this.t[newest]) return c[newest];
68
+ let prev = oldest;
69
+ for (let i = 1; i < this.count; i++) {
70
+ const idx = (oldest + i) % this.cap;
71
+ if (this.t[idx] >= time) {
72
+ if (this.step) return c[prev];
73
+ const t0 = this.t[prev], t1 = this.t[idx];
74
+ const a = t1 > t0 ? (time - t0) / (t1 - t0) : 0;
75
+ return c[prev] + (c[idx] - c[prev]) * a;
76
+ }
77
+ prev = idx;
78
+ }
79
+ return c[newest];
80
+ }
81
+ };
82
+ function historyForField(instance, field) {
83
+ const arr = instance[$HISTORY];
84
+ if (arr === void 0) return void 0;
85
+ for (let i = 0; i < arr.length; i++) {
86
+ if (arr[i].fields.indexOf(field) >= 0) return arr[i];
87
+ }
88
+ return void 0;
89
+ }
90
+ var RewindView = class {
91
+ constructor() {
92
+ this._time = 0;
93
+ this._reckonTime = 0;
94
+ }
95
+ /** The clamped server-time (ms) this view reads at — after the maxRewindMs
96
+ * clamp and the live (newest-sample) fallback. Mostly for logging/debug. */
97
+ get time() {
98
+ return this._time;
99
+ }
100
+ /** The clamped server-time (ms) `mode: "reckon"` groups read at — the
101
+ * client's reconstructed simulation instant, not the raw stamp (see
102
+ * {@link Rewind.at}). Always a resolved instant: a clamped direct stamp,
103
+ * or the midpoint/live reconstruction when unstamped. `0` only before the
104
+ * first `record()`. */
105
+ get reckonTime() {
106
+ return this._reckonTime;
107
+ }
108
+ /** @internal Aim at a rewind + clamped times ({@link Rewind.at} owns the clamps). */
109
+ _retarget(rewind, at, reckonAt) {
110
+ this.rewind = rewind;
111
+ this._time = at;
112
+ this._reckonTime = reckonAt;
113
+ return this;
114
+ }
115
+ /** Rewound value of a numeric `field` on `entity` (live if it isn't tracked). */
116
+ value(entity, field) {
117
+ if (this.rewind === void 0) {
118
+ throw new Error("RewindView is not aimed \u2014 obtain it from rewind.at()/lastSeenBy(), or pass it to them as `out`.");
119
+ }
120
+ const h = historyForField(entity, field);
121
+ if (h === void 0) return entity[field];
122
+ return h.valueAt(h.reckoned ? this._reckonTime : this._time, h.fields.indexOf(field));
123
+ }
124
+ /**
125
+ * Batch {@link value} reads: the `fields` YOU list define the result's shape
126
+ * (`Record<field, number>`). Pass `out` to fill (and return) a reused scratch
127
+ * instead of allocating — its properties beyond `fields` are left untouched,
128
+ * so a scratch can carry extra context (an `alive` flag, say).
129
+ */
130
+ read(entity, fields, out) {
131
+ const o = out ?? {};
132
+ for (let i = 0; i < fields.length; i++) o[fields[i]] = this.value(entity, fields[i]);
133
+ return o;
134
+ }
135
+ };
136
+ var Rewind = class _Rewind {
137
+ constructor(defaultMaxRewindMs) {
138
+ this.groups = [];
139
+ this._sampleIntervalMs = DEFAULT_SAMPLE_INTERVAL_MS;
140
+ this._lastRecordedAt = -1;
141
+ /** Default view returned by at/lastSeenBy when no `out` is given — one per
142
+ * Rewind, re-aimed per call (zero alloc, zero userland setup). */
143
+ this._view = new RewindView();
144
+ this.defaultMaxRewindMs = defaultMaxRewindMs;
145
+ }
146
+ static {
147
+ this.byRoom = /* @__PURE__ */ new WeakMap();
148
+ }
149
+ /** One `Rewind` per room (idempotent). `room` is only the cache key. */
150
+ static get(room, opts) {
151
+ let r = _Rewind.byRoom.get(room);
152
+ if (r === void 0) {
153
+ r = new _Rewind(opts?.maxRewindMs ?? DEFAULT_MAX_REWIND_MS);
154
+ _Rewind.byRoom.set(room, r);
155
+ }
156
+ return r;
157
+ }
158
+ /** The default rewind window (ms). Use it to bound a hit test:
159
+ * `Math.max(renderTime, now - rewind.maxRewindMs)`. */
160
+ get maxRewindMs() {
161
+ return this.defaultMaxRewindMs;
162
+ }
163
+ /** Server time of the last {@link record} (or -1). The auto-record skips a tick
164
+ * whose time was already recorded manually — your `record()` wins. */
165
+ get lastRecordedAt() {
166
+ return this._lastRecordedAt;
167
+ }
168
+ /** Track every entity in a collection (Map/Array/Set schema). `fields` narrows
169
+ * to its element's numeric fields. `interpolate` (a mode or a per-entity fn,
170
+ * default `linear`) picks how `valueAt` reconstructs between samples — use
171
+ * `step` for discrete-motion entities (e.g. teleporters). `mode` picks the
172
+ * lag-comp timeline (default `"snapshot"`; see {@link AttachOptions}). */
173
+ attachAll(collection, opts) {
174
+ const c = collection;
175
+ this.groups.push({ entities: () => c.values(), fields: opts.fields, resolvedByCtor: /* @__PURE__ */ new Map(), maxRewindMs: opts.maxRewindMs ?? this.defaultMaxRewindMs, interpolate: opts.interpolate ?? "linear", reckoned: opts.mode === "reckon", groupId: this.groups.length });
176
+ return this;
177
+ }
178
+ /** Track a single entity (e.g. a boss). `fields` narrows to its numeric fields. */
179
+ attach(instance, opts) {
180
+ const one = [instance];
181
+ this.groups.push({ entities: () => one, fields: opts.fields, resolvedByCtor: /* @__PURE__ */ new Map(), maxRewindMs: opts.maxRewindMs ?? this.defaultMaxRewindMs, interpolate: opts.interpolate ?? "linear", reckoned: opts.mode === "reckon", groupId: this.groups.length });
182
+ return this;
183
+ }
184
+ /**
185
+ * Snapshot every tracked entity at `now`. Call once per tick, AFTER they move —
186
+ * {@link Room.allowRewindState} does this for you. `sampleIntervalMs` (the gap
187
+ * between records) sizes the history rings; the framework passes the sim
188
+ * interval. Calling this yourself during a tick suppresses that tick's
189
+ * auto-record (see {@link lastRecordedAt}).
190
+ */
191
+ record(now, sampleIntervalMs) {
192
+ if (sampleIntervalMs !== void 0 && sampleIntervalMs > 0) this._sampleIntervalMs = sampleIntervalMs;
193
+ this._lastRecordedAt = now;
194
+ for (const g of this.groups) {
195
+ for (const e of g.entities()) {
196
+ const t = e;
197
+ let arr = t[$HISTORY];
198
+ if (arr === void 0) {
199
+ arr = [];
200
+ t[$HISTORY] = arr;
201
+ }
202
+ let h;
203
+ for (let i = 0; i < arr.length; i++) {
204
+ if (arr[i].groupId === g.groupId) {
205
+ h = arr[i];
206
+ break;
207
+ }
208
+ }
209
+ if (h === void 0) {
210
+ h = this.createHistory(g, e);
211
+ arr.push(h);
212
+ }
213
+ h.record(now, t[import_schema.$values]);
214
+ }
215
+ }
216
+ }
217
+ /** Cold path — first time an entity is seen by `record()` for a given group.
218
+ * Resolves the field→`$values` indices (cached per constructor for array-form
219
+ * `fields`; fn-form resolves per entity) and bakes in the group's `mode`. */
220
+ createHistory(g, e) {
221
+ let rf;
222
+ if (typeof g.fields === "function") {
223
+ rf = this.resolveFields(e, g.fields(e));
224
+ } else {
225
+ let cached = g.resolvedByCtor.get(e.constructor);
226
+ if (cached === void 0) {
227
+ cached = this.resolveFields(e, g.fields);
228
+ g.resolvedByCtor.set(e.constructor, cached);
229
+ }
230
+ rf = cached;
231
+ }
232
+ const interp = typeof g.interpolate === "function" ? g.interpolate(e) : g.interpolate;
233
+ return new EntityHistory(rf.fields, rf.idx, g.maxRewindMs, this._sampleIntervalMs, interp === "step", g.reckoned, g.groupId);
234
+ }
235
+ /** Keep only the fields this entity's type declares (unknown → read live). */
236
+ resolveFields(e, names) {
237
+ const fields = [];
238
+ const idx = [];
239
+ for (const name of names) {
240
+ const i = fieldIndexOf(e, name);
241
+ if (i >= 0) {
242
+ fields.push(name);
243
+ idx.push(i);
244
+ }
245
+ }
246
+ return { fields, idx };
247
+ }
248
+ /**
249
+ * `instance`'s `field` value at past `time` (interpolated from history). Falls
250
+ * back to the live value when the entity has no history yet, or the field
251
+ * isn't tracked.
252
+ */
253
+ valueAt(instance, time, field) {
254
+ const h = historyForField(instance, field);
255
+ if (h === void 0) return instance[field];
256
+ return h.valueAt(time, h.fields.indexOf(field));
257
+ }
258
+ /**
259
+ * Which timeline(s) the attached groups rewind to — `reckon` true iff any
260
+ * group is `mode:"reckon"`, `snapshot` true iff any is `mode:"snapshot"`. The
261
+ * room reads this once (groups are declared in `onCreate`, before any join) to
262
+ * pick which stamp(s) the client must ship: reckon-only / render-only / both /
263
+ * none. @internal
264
+ */
265
+ timelineMode() {
266
+ let reckon = false, snapshot = false;
267
+ for (const g of this.groups) {
268
+ if (g.reckoned) reckon = true;
269
+ else snapshot = true;
270
+ }
271
+ return { reckon, snapshot };
272
+ }
273
+ /**
274
+ * @internal Wire a resolver from sessionId → that client's auto-stamped
275
+ * `renderTime`. Called by {@link Room.allowRewindState} so {@link lastSeenBy}
276
+ * works for `mode:"snapshot"` groups; auto-enabled by attaching one.
277
+ */
278
+ bindRenderTime(resolver) {
279
+ this._renderTimeOf = resolver;
280
+ }
281
+ /**
282
+ * @internal Wire a resolver for the CURRENT server time (ms, same epoch as
283
+ * `record` times). Called by {@link Room.allowRewindState}; standalone users
284
+ * (tests/harnesses) should bind their own. Anchors the reckon midpoint in
285
+ * {@link at} at the true processing instant — the `lastRecordedAt` fallback
286
+ * is one tick stale by hit-test time, biasing the aim ~half a tick early:
287
+ * imperceptible on slow horizontal motion, but enough to flip knife-edge
288
+ * stomp/hit verdicts on fast vertical movers (a bobbing jumper).
289
+ */
290
+ bindNow(resolver) {
291
+ this._nowOf = resolver;
292
+ }
293
+ /**
294
+ * @internal Wire a resolver from sessionId → that client's auto-stamped
295
+ * reckon-display instant (`input(sid).reckonTime`). Called by
296
+ * {@link Room.allowRewindState}. With it, {@link lastSeenBy} aims
297
+ * `mode:"reckon"` groups at the EXACT instant the client displayed them
298
+ * (immune to its RTT-estimation error); without it (or while the stamp is
299
+ * still 0), the midpoint reconstruction in {@link at} is the fallback.
300
+ */
301
+ bindReckonTime(resolver) {
302
+ this._reckonTimeOf = resolver;
303
+ }
304
+ /**
305
+ * A {@link RewindView} of the tracked world at `time` (an acting client's render
306
+ * time), with the two things every hit test re-implements baked in:
307
+ * - **clamp** to `[lastRecordedAt − maxRewindMs, lastRecordedAt]` — an
308
+ * anti-spoof / clock-skew bound (a client can't rewind arbitrarily far), and
309
+ * - **live fallback**: `time <= 0` (the client's clock hasn't synced) → the
310
+ * newest recorded sample (≈ current position).
311
+ *
312
+ * Sugar over {@link valueAt}; pass the render time yourself (e.g. from
313
+ * `input(sid).renderTime`, or a value stored on the entity). For the common
314
+ * "rewind to a specific client's view" case use {@link lastSeenBy}.
315
+ *
316
+ * Re-aims and returns this Rewind's internal view by default — zero alloc,
317
+ * nothing to declare. Pass `out` to aim a view of your own instead; needed
318
+ * only to hold several views at once — see the {@link RewindView} doc.
319
+ */
320
+ at(time, out) {
321
+ return this._aim(time, 0, out);
322
+ }
323
+ /**
324
+ * Shared aiming: clamp the snapshot-timeline `time`, and resolve the reckon
325
+ * timeline either from the DIRECT `reckonStamp` (the instant the client's
326
+ * forward-reckoned entities were displayed at — exact, immune to the
327
+ * client's RTT-estimation error) or, when absent (0), by reconstruction.
328
+ */
329
+ _aim(time, reckonStamp, out) {
330
+ const newest = this._lastRecordedAt;
331
+ const oldest = newest - this.defaultMaxRewindMs;
332
+ const at = time <= 0 ? newest : time < oldest ? oldest : time > newest ? newest : time;
333
+ let reckonAt;
334
+ if (reckonStamp > 0) {
335
+ reckonAt = reckonStamp < oldest ? oldest : reckonStamp > newest ? newest : reckonStamp;
336
+ } else if (time <= 0) {
337
+ reckonAt = newest;
338
+ } else {
339
+ const anchor = this._nowOf !== void 0 ? this._nowOf() : newest;
340
+ const mid = (time + anchor) / 2;
341
+ reckonAt = mid < oldest ? oldest : mid > newest ? newest : mid;
342
+ }
343
+ return (out ?? this._view)._retarget(this, at, reckonAt);
344
+ }
345
+ /**
346
+ * A {@link RewindView} of the world as session `sessionId` LAST saw it —
347
+ * resolves the render time stamped on that client's most recently CONSUMED
348
+ * input (hence "last": under a multi-frame `drain()` it is the newest frame's
349
+ * stamp) and hands off to {@link at}. This is the "what you see is what you
350
+ * hit" path:
351
+ *
352
+ * ```ts
353
+ * const seen = this.rewind.lastSeenBy(shooterSessionId);
354
+ * const hit = overlaps(bullet, seen.value(target, "x"), seen.value(target, "y"));
355
+ * ```
356
+ *
357
+ * Requires the room's framework input (`this.defineInput(Input)`) — the
358
+ * per-client stamp auto-enables from the `attachAll` `mode` of the groups you
359
+ * rewind (declaration order vs `allowRewindState` doesn't matter). A client
360
+ * that merely hasn't stamped yet (clock still syncing, unknown sessionId) is
361
+ * NOT an error — it reads 0 and the view falls back to live. Use
362
+ * {@link at} with an explicit time if you stamp render times yourself.
363
+ *
364
+ * Re-aims and returns this Rewind's internal view by default — zero alloc,
365
+ * nothing to declare. Pass `out` to aim a view of your own instead; needed
366
+ * only to hold several views at once — see the {@link RewindView} doc.
367
+ */
368
+ lastSeenBy(sessionId, out) {
369
+ if (this._renderTimeOf === void 0) {
370
+ throw new Error(
371
+ "Rewind.lastSeenBy(sessionId) needs the framework input API. Declare `this.defineInput(Input)` (stamps auto-enable from your attachAll `mode`), or call `at(time)` and pass the render time yourself."
372
+ );
373
+ }
374
+ return this._aim(
375
+ this._renderTimeOf(sessionId),
376
+ this._reckonTimeOf !== void 0 ? this._reckonTimeOf(sessionId) : 0,
377
+ out
378
+ );
379
+ }
380
+ };
381
+ // Annotate the CommonJS export names for ESM import in node:
382
+ 0 && (module.exports = {
383
+ Rewind,
384
+ RewindView
385
+ });