@dcl/sdk 7.20.2-22104870534.commit-0df3cc0 → 7.20.2-22169778016.commit-030cbfe

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 (63) hide show
  1. package/atom.d.ts +19 -0
  2. package/atom.js +83 -0
  3. package/future.d.ts +8 -0
  4. package/future.js +26 -0
  5. package/network/binary-message-bus.d.ts +6 -3
  6. package/network/binary-message-bus.js +9 -5
  7. package/network/chunking.d.ts +5 -0
  8. package/network/chunking.js +38 -0
  9. package/network/events/implementation.d.ts +93 -0
  10. package/network/events/implementation.js +230 -0
  11. package/network/events/index.d.ts +42 -0
  12. package/network/events/index.js +43 -0
  13. package/network/events/protocol.d.ts +27 -0
  14. package/network/events/protocol.js +66 -0
  15. package/network/events/registry.d.ts +8 -0
  16. package/network/events/registry.js +3 -0
  17. package/network/index.d.ts +8 -2
  18. package/network/index.js +16 -3
  19. package/network/message-bus-sync.d.ts +14 -1
  20. package/network/message-bus-sync.js +166 -103
  21. package/network/server/index.d.ts +14 -0
  22. package/network/server/index.js +219 -0
  23. package/network/server/utils.d.ts +18 -0
  24. package/network/server/utils.js +135 -0
  25. package/network/state.js +3 -5
  26. package/package.json +6 -6
  27. package/server/env-var.d.ts +15 -0
  28. package/server/env-var.js +31 -0
  29. package/server/index.d.ts +2 -0
  30. package/server/index.js +3 -0
  31. package/server/storage/constants.d.ts +23 -0
  32. package/server/storage/constants.js +2 -0
  33. package/server/storage/index.d.ts +22 -0
  34. package/server/storage/index.js +29 -0
  35. package/server/storage/player.d.ts +43 -0
  36. package/server/storage/player.js +92 -0
  37. package/server/storage/scene.d.ts +38 -0
  38. package/server/storage/scene.js +90 -0
  39. package/server/storage-url.d.ts +10 -0
  40. package/server/storage-url.js +29 -0
  41. package/server/utils.d.ts +35 -0
  42. package/server/utils.js +56 -0
  43. package/src/atom.ts +98 -0
  44. package/src/future.ts +38 -0
  45. package/src/network/binary-message-bus.ts +9 -4
  46. package/src/network/chunking.ts +45 -0
  47. package/src/network/events/implementation.ts +286 -0
  48. package/src/network/events/index.ts +48 -0
  49. package/src/network/events/protocol.ts +94 -0
  50. package/src/network/events/registry.ts +18 -0
  51. package/src/network/index.ts +40 -3
  52. package/src/network/message-bus-sync.ts +180 -110
  53. package/src/network/server/index.ts +301 -0
  54. package/src/network/server/utils.ts +189 -0
  55. package/src/network/state.ts +3 -4
  56. package/src/server/env-var.ts +36 -0
  57. package/src/server/index.ts +2 -0
  58. package/src/server/storage/constants.ts +22 -0
  59. package/src/server/storage/index.ts +44 -0
  60. package/src/server/storage/player.ts +156 -0
  61. package/src/server/storage/scene.ts +149 -0
  62. package/src/server/storage-url.ts +34 -0
  63. package/src/server/utils.ts +73 -0
@@ -1,27 +1,53 @@
1
- import { IEngine, Transport, RealmInfo, PlayerIdentityData } from '@dcl/ecs'
1
+ import { IEngine, Transport, RealmInfo } from '@dcl/ecs'
2
2
  import { type SendBinaryRequest, type SendBinaryResponse } from '~system/CommunicationsController'
3
3
 
4
4
  import { syncFilter } from './filter'
5
5
  import { engineToCrdt } from './state'
6
- import { BinaryMessageBus, CommsMessage, decodeString, encodeString } from './binary-message-bus'
6
+ import { BinaryMessageBus, CommsMessage } from './binary-message-bus'
7
7
  import { fetchProfile } from './utils'
8
8
  import { entityUtils } from './entities'
9
+ import { createServerValidator } from './server'
9
10
  import { GetUserDataRequest, GetUserDataResponse } from '~system/UserIdentity'
10
11
  import { definePlayerHelper } from '../players'
11
12
  import { serializeCrdtMessages } from '../internal/transports/logger'
13
+ import { IsServerRequest, IsServerResponse } from '~system/EngineApi'
14
+ import { Atom } from '../atom'
15
+ import { setGlobalRoom, Room } from './events/implementation'
12
16
 
13
17
  export type IProfile = { networkId: number; userId: string }
14
18
  // user that we asked for the inital crdt state
19
+ export const AUTH_SERVER_PEER_ID = 'authoritative-server'
20
+ export const DEBUG_NETWORK_MESSAGES = () => (globalThis as any).DEBUG_NETWORK_MESSAGES ?? false
21
+
22
+ // Test environment detection without 'as any'
23
+ const isTestEnvironment = (): boolean => {
24
+ try {
25
+ if (typeof globalThis === 'undefined') return false
26
+ const globalWithProcess = globalThis as unknown as { process?: { env?: { NODE_ENV?: string } } }
27
+ return globalWithProcess.process?.env?.NODE_ENV === 'test'
28
+ } catch {
29
+ return false
30
+ }
31
+ }
32
+
15
33
  export function addSyncTransport(
16
34
  engine: IEngine,
17
35
  sendBinary: (msg: SendBinaryRequest) => Promise<SendBinaryResponse>,
18
- getUserData: (value: GetUserDataRequest) => Promise<GetUserDataResponse>
36
+ getUserData: (value: GetUserDataRequest) => Promise<GetUserDataResponse>,
37
+ isServerFn: (request: IsServerRequest) => Promise<IsServerResponse>,
38
+ name: string
19
39
  ) {
20
- const DEBUG_NETWORK_MESSAGES = () => (globalThis as any).DEBUG_NETWORK_MESSAGES ?? false
21
40
  // Profile Info
22
41
  const myProfile: IProfile = {} as IProfile
23
42
  fetchProfile(myProfile!, getUserData)
24
43
 
44
+ const isServerAtom = Atom<boolean>()
45
+ const isRoomReadyAtom = Atom<boolean>(false)
46
+
47
+ void isServerFn({}).then(($: IsServerResponse) => {
48
+ return isServerAtom.swap(!!$.isServer)
49
+ })
50
+
25
51
  // Entity utils
26
52
  const entityDefinitions = entityUtils(engine, myProfile)
27
53
 
@@ -40,108 +66,196 @@ export function addSyncTransport(
40
66
  const players = definePlayerHelper(engine)
41
67
 
42
68
  let stateIsSyncronized = false
43
- let transportInitialzed = false
44
69
 
70
+ /**
71
+ * We need to wait till 2 ticks that is when the engine is ready to send new messages.
72
+ * The first tick is for the client engine processing the CRDT messages,
73
+ * and the second one are the messages created by the main() function.
74
+ * So to avoid sending those messages, that all the clients have, through the network we put this validation here.
75
+ */
76
+ let tick = 0
77
+ const TRANSPORT_INITIALIZED_NUMBER = isTestEnvironment() ? 0 : 2
45
78
  // Add Sync Transport
46
79
  const transport: Transport = {
47
80
  filter: syncFilter(engine),
48
81
  send: async (messages) => {
49
- for (const message of [messages].flat()) {
50
- if (message.byteLength && transportInitialzed) {
82
+ if (tick <= TRANSPORT_INITIALIZED_NUMBER) tick++
83
+ for (const message of tick > TRANSPORT_INITIALIZED_NUMBER ? [messages].flat() : []) {
84
+ if (message.byteLength) {
51
85
  DEBUG_NETWORK_MESSAGES() &&
52
86
  console.log(...Array.from(serializeCrdtMessages('[NetworkMessage sent]:', message, engine)))
53
- binaryMessageBus.emit(CommsMessage.CRDT, message)
87
+
88
+ // Convert regular messages to network messages for broadcasting with chunking
89
+ for (const chunk of serverValidator.convertRegularToNetworkMessage(message)) {
90
+ binaryMessageBus.emit(CommsMessage.CRDT, chunk)
91
+ }
54
92
  }
55
93
  }
56
94
  const peerMessages = getMessagesToSend()
57
- let totalSize = 0
58
- for (const message of peerMessages) {
59
- for (const data of message.data) {
60
- totalSize += data.byteLength
61
- }
62
- }
63
- if (totalSize) {
64
- DEBUG_NETWORK_MESSAGES() && console.log('Sending network messages: ', totalSize / 1024, 'KB')
65
- }
66
95
  const response = await sendBinary({ data: [], peerData: peerMessages })
67
96
  binaryMessageBus.__processMessages(response.data)
68
- transportInitialzed = true
69
97
  },
70
- type: 'network'
98
+ type: name
71
99
  }
100
+
101
+ // Server validation setup
102
+ const serverValidator = createServerValidator({
103
+ engine,
104
+ binaryMessageBus
105
+ })
106
+
107
+ // Initialize Event Bus with registered schemas
108
+ const eventBus = new Room(engine, binaryMessageBus, isServerAtom, isRoomReadyAtom)
109
+
110
+ // Set global eventBus instance
111
+ setGlobalRoom(eventBus)
112
+
72
113
  engine.addTransport(transport)
73
114
  // End add sync transport
74
115
 
75
116
  // Receive & Process CRDT_STATE
76
- binaryMessageBus.on(CommsMessage.RES_CRDT_STATE, (value) => {
77
- const { sender, data } = decodeCRDTState(value)
78
- if (sender !== myProfile.userId) return
117
+ binaryMessageBus.on(CommsMessage.REQ_CRDT_STATE, async (data, sender) => {
118
+ DEBUG_NETWORK_MESSAGES() && console.log('[REQ_CRDT_STATE]', sender, Date.now())
119
+ for (const chunk of engineToCrdt(engine)) {
120
+ DEBUG_NETWORK_MESSAGES() && console.log('[Emiting:]', sender, Date.now())
121
+ binaryMessageBus.emit(CommsMessage.RES_CRDT_STATE, chunk, [sender])
122
+ }
123
+ })
124
+ binaryMessageBus.on(CommsMessage.RES_CRDT_STATE, async (data, sender) => {
125
+ requestingState = false
126
+ elapsedTimeSinceRequest = 0
127
+ if (isServerAtom.getOrNull() || sender !== AUTH_SERVER_PEER_ID) return
79
128
  DEBUG_NETWORK_MESSAGES() && console.log('[Processing CRDT State]', data.byteLength / 1024, 'KB')
80
- transport.onmessage!(data)
129
+ transport.onmessage!(serverValidator.processClientMessages(data, sender))
81
130
  stateIsSyncronized = true
82
- })
83
-
84
- // Answer to REQ_CRDT_STATE
85
- binaryMessageBus.on(CommsMessage.REQ_CRDT_STATE, async (_, userId) => {
86
- DEBUG_NETWORK_MESSAGES() && console.log(`Sending CRDT State to: ${userId}`)
87
131
 
88
- for (const chunk of engineToCrdt(engine)) {
89
- binaryMessageBus.emit(CommsMessage.RES_CRDT_STATE, encodeCRDTState(userId, chunk), [userId])
132
+ // IMPORTANT: Only mark room as ready AFTER state is synchronized
133
+ // This ensures comms is truly connected and working
134
+ const realmInfo = RealmInfo.getOrNull(engine.RootEntity)
135
+ if (realmInfo && checkRoomReady(realmInfo)) {
136
+ DEBUG_NETWORK_MESSAGES() && console.log('[isRoomReady] Marking room as ready after state sync')
137
+ isRoomReadyAtom.swap(true)
90
138
  }
91
139
  })
92
140
 
93
- // Process CRDT messages here
94
- binaryMessageBus.on(CommsMessage.CRDT, (value) => {
141
+ // received message from the network
142
+ binaryMessageBus.on(CommsMessage.CRDT, (value, sender) => {
143
+ const isServer = isServerAtom.getOrNull()
95
144
  DEBUG_NETWORK_MESSAGES() &&
96
- console.log(Array.from(serializeCrdtMessages('[NetworkMessage received]:', value, engine)))
97
- transport.onmessage!(value)
98
- })
99
-
100
- async function requestState(retryCount: number = 1) {
101
- let players = Array.from(engine.getEntitiesWith(PlayerIdentityData))
102
- DEBUG_NETWORK_MESSAGES() && console.log(`Requesting state. Players connected: ${players.length - 1}`)
103
-
104
- if (!RealmInfo.getOrNull(engine.RootEntity)?.isConnectedSceneRoom) {
105
- DEBUG_NETWORK_MESSAGES() && console.log(`Aborting Requesting state?. Disconnected`)
106
- return
145
+ console.log(
146
+ transport.type,
147
+ ...Array.from(serializeCrdtMessages('[NetworkMessage received]:', value, engine)),
148
+ isServer
149
+ )
150
+ if (isServer) {
151
+ transport.onmessage!(serverValidator.processServerMessages(value, sender))
152
+ } else if (sender === AUTH_SERVER_PEER_ID) {
153
+ // Process network messages from server and convert to regular messages
154
+ transport.onmessage!(serverValidator.processClientMessages(value, sender))
107
155
  }
156
+ })
108
157
 
109
- binaryMessageBus.emit(CommsMessage.REQ_CRDT_STATE, new Uint8Array())
158
+ // received authoritative message from server - force apply to fix invalid local state
159
+ binaryMessageBus.on(CommsMessage.CRDT_AUTHORITATIVE, (value, sender) => {
160
+ // Only accept authoritative messages from authoritative server
161
+ if (sender !== AUTH_SERVER_PEER_ID) return
110
162
 
111
- // Wait ~5s for the response.
112
- await sleep(5000)
163
+ // DEBUG_NETWORK_MESSAGES() &&
164
+ console.log('[AUTHORITATIVE] Received authoritative message from server:', value.byteLength, 'bytes')
113
165
 
114
- players = Array.from(engine.getEntitiesWith(PlayerIdentityData))
166
+ // Process authoritative messages by forcing them through normal CRDT processing
167
+ // but with a timestamp that's guaranteed to be accepted
168
+ const authoritativeBuffer = serverValidator.processClientMessages(value, sender, true)
169
+ if (authoritativeBuffer.byteLength > 0) {
170
+ // Apply authoritative message through normal transport, but the server's messages
171
+ // should be processed as authoritative with special timestamp handling
172
+ transport.onmessage!(authoritativeBuffer)
115
173
 
116
- if (!stateIsSyncronized) {
117
- if (players.length > 1 && retryCount <= 2) {
118
- DEBUG_NETWORK_MESSAGES() &&
119
- console.log(`Requesting state again ${retryCount} (no response). Players connected: ${players.length - 1}`)
120
- void requestState(retryCount + 1)
121
- } else {
122
- DEBUG_NETWORK_MESSAGES() && console.log('No active players. State syncronized')
123
- stateIsSyncronized = true
124
- }
174
+ DEBUG_NETWORK_MESSAGES() && console.log('[AUTHORITATIVE] Applied server authoritative message to local state')
125
175
  }
126
- }
176
+ })
127
177
 
128
178
  players.onEnterScene((player) => {
129
179
  DEBUG_NETWORK_MESSAGES() && console.log('[onEnterScene]', player.userId)
180
+ if (!isServerAtom.getOrNull() && myProfile.userId === player.userId) {
181
+ requestState()
182
+ }
130
183
  })
131
184
 
185
+ // Helper to check room ready conditions
186
+ function checkRoomReady(realmInfo: ReturnType<typeof RealmInfo.getOrNull>): boolean {
187
+ if (!realmInfo) return false
188
+
189
+ try {
190
+ // Check if room instance exists
191
+ if (!eventBus) return false
192
+
193
+ return !!(realmInfo.commsAdapter && realmInfo.isConnectedSceneRoom && realmInfo.room)
194
+ } catch {
195
+ return false
196
+ }
197
+ }
198
+
132
199
  // Asks for the REQ_CRDT_STATE when its connected to comms
133
200
  RealmInfo.onChange(engine.RootEntity, (value) => {
201
+ const isServer = isServerAtom.getOrNull()
202
+
134
203
  if (!value?.isConnectedSceneRoom) {
135
- DEBUG_NETWORK_MESSAGES() && console.log('Disconnected from comms')
136
- stateIsSyncronized = false
204
+ // Only react when actually transitioning from ready to not ready
205
+ if (isRoomReadyAtom.getOrNull() === true) {
206
+ DEBUG_NETWORK_MESSAGES() && console.log('Disconnected from comms')
207
+ isRoomReadyAtom.swap(false)
208
+ if (!isServer) {
209
+ stateIsSyncronized = false
210
+ }
211
+ }
137
212
  }
138
213
 
139
214
  if (value?.isConnectedSceneRoom) {
140
- DEBUG_NETWORK_MESSAGES() && console.log('Connected to comms')
215
+ requestState()
216
+
217
+ // For servers, mark as ready immediately when connected
218
+ // (servers don't need to sync state from anyone)
219
+ if (isServer && checkRoomReady(value) && isRoomReadyAtom.getOrNull() === false) {
220
+ DEBUG_NETWORK_MESSAGES() && console.log('[isRoomReady] Server marking room as ready')
221
+ isRoomReadyAtom.swap(true)
222
+ }
223
+ // For clients, room will be marked ready after receiving CRDT state (above)
141
224
  }
225
+ })
142
226
 
143
- if (value?.isConnectedSceneRoom && !stateIsSyncronized) {
144
- void requestState()
227
+ let requestingState = false
228
+ let elapsedTimeSinceRequest = 0
229
+ const STATE_REQUEST_RETRY_INTERVAL = 2.0 // seconds
230
+
231
+ /**
232
+ * Why we have to request the state if we have a server that can send us the state when we joined?
233
+ * The thing is that when the server detects a new JOIN_PARTICIPANT on livekit room, it sends automatically the state to that peer.
234
+ * But in unity, it takes more time, so that message is not being delivered to the client.
235
+ * So instead, when we are finally connected to the room, we request the state, and then the server answers with the state :)
236
+ *
237
+ * If no response is received within 2 seconds, the request is automatically retried.
238
+ */
239
+ function requestState() {
240
+ if (isServerAtom.getOrNull()) return
241
+ if (RealmInfo.getOrNull(engine.RootEntity)?.isConnectedSceneRoom && !requestingState) {
242
+ requestingState = true
243
+ elapsedTimeSinceRequest = 0
244
+ DEBUG_NETWORK_MESSAGES() && console.log('Requesting state...')
245
+ binaryMessageBus.emit(CommsMessage.REQ_CRDT_STATE, new Uint8Array())
246
+ }
247
+ }
248
+
249
+ // System to retry state request if no response is received within the retry interval
250
+ engine.addSystem((dt: number) => {
251
+ if (requestingState && !stateIsSyncronized) {
252
+ elapsedTimeSinceRequest += dt
253
+ if (elapsedTimeSinceRequest >= STATE_REQUEST_RETRY_INTERVAL) {
254
+ DEBUG_NETWORK_MESSAGES() && console.log('State request timed out, retrying...')
255
+ elapsedTimeSinceRequest = 0
256
+ requestingState = false
257
+ requestState()
258
+ }
145
259
  }
146
260
  })
147
261
 
@@ -153,56 +267,12 @@ export function addSyncTransport(
153
267
  return stateIsSyncronized
154
268
  }
155
269
 
156
- function sleep(ms: number) {
157
- return new Promise<void>((resolve) => {
158
- let timer = 0
159
- function sleepSystem(dt: number) {
160
- timer += dt
161
- if (timer * 1000 >= ms) {
162
- engine.removeSystem(sleepSystem)
163
- resolve()
164
- }
165
- }
166
- engine.addSystem(sleepSystem)
167
- })
168
- }
169
-
170
270
  return {
171
271
  ...entityDefinitions,
172
272
  myProfile,
173
- isStateSyncronized
273
+ isStateSyncronized,
274
+ binaryMessageBus,
275
+ eventBus,
276
+ isRoomReadyAtom
174
277
  }
175
278
  }
176
-
177
- /**
178
- * Messages Protocol Encoding
179
- *
180
- * CRDT: Plain Uint8Array
181
- *
182
- * CRDT_STATE_RES { sender: string, data: Uint8Array}
183
- */
184
- function decodeCRDTState(data: Uint8Array) {
185
- let offset = 0
186
- const r = new Uint8Array(data)
187
- const view = new DataView(r.buffer)
188
- const senderLength = view.getUint8(offset)
189
- offset += 1
190
- const sender = decodeString(data.subarray(1, senderLength + 1))
191
- offset += senderLength
192
- const state = r.subarray(offset)
193
-
194
- return { sender, data: state }
195
- }
196
-
197
- function encodeCRDTState(address: string, data: Uint8Array) {
198
- // address to uint8array
199
- const addressBuffer = encodeString(address)
200
- const addressOffset = 1
201
- const messageLength = addressOffset + addressBuffer.byteLength + data.byteLength
202
-
203
- const serializedMessage = new Uint8Array(messageLength)
204
- serializedMessage.set(new Uint8Array([addressBuffer.byteLength]), 0)
205
- serializedMessage.set(addressBuffer, 1)
206
- serializedMessage.set(data, addressBuffer.byteLength + 1)
207
- return serializedMessage
208
- }
@@ -0,0 +1,301 @@
1
+ import {
2
+ IEngine,
3
+ Entity,
4
+ CrdtMessageType,
5
+ CrdtMessageBody,
6
+ ProcessMessageResultType,
7
+ ComponentType,
8
+ PutNetworkComponentOperation
9
+ } from '@dcl/ecs'
10
+ import * as components from '@dcl/ecs/dist/components'
11
+ import { ReadWriteByteBuffer } from '@dcl/ecs/dist/serialization/ByteBuffer'
12
+ import { CommsMessage } from '../binary-message-bus'
13
+ import { chunkCrdtMessages } from '../chunking'
14
+ import * as utils from './utils'
15
+ import { AUTH_SERVER_PEER_ID, DEBUG_NETWORK_MESSAGES } from '../message-bus-sync'
16
+ import { type BinaryMessageBus } from '../binary-message-bus'
17
+ import {
18
+ LastWriteWinElementSetComponentDefinition,
19
+ GrowOnlyValueSetComponentDefinition,
20
+ ComponentDefinition,
21
+ InternalBaseComponent
22
+ } from '@dcl/ecs/dist/engine/component'
23
+
24
+ export const LIVEKIT_MAX_SIZE = 12
25
+
26
+ export interface ServerValidationConfig {
27
+ engine: IEngine
28
+ binaryMessageBus: ReturnType<typeof BinaryMessageBus>
29
+ }
30
+
31
+ export function createServerValidator(config: ServerValidationConfig) {
32
+ const { engine, binaryMessageBus } = config
33
+
34
+ // Initialize components for network operations and transform fixing
35
+ const NetworkEntity = components.NetworkEntity(engine)
36
+ const CreatedBy = components.CreatedBy(engine)
37
+ const NetworkParent = components.NetworkParent(engine)
38
+
39
+ // Type guard to check if component supports corrections (both LWW and GrowOnlySet)
40
+ function supportsCorrections<T>(
41
+ component: ComponentDefinition<T>
42
+ ): component is LastWriteWinElementSetComponentDefinition<T> | GrowOnlyValueSetComponentDefinition<T> {
43
+ return (
44
+ (component.componentType === ComponentType.LastWriteWinElementSet ||
45
+ component.componentType === ComponentType.GrowOnlyValueSet) &&
46
+ 'getCrdtState' in component
47
+ )
48
+ }
49
+
50
+ function findExistingNetworkEntity(message: utils.NetworkMessage): Entity | null {
51
+ // Look for existing network entity mapping (don't create new ones)
52
+ for (const [entityId, networkData] of engine.getEntitiesWith(NetworkEntity)) {
53
+ if (networkData.networkId === message.networkId && networkData.entityId === message.entityId) {
54
+ return entityId
55
+ }
56
+ }
57
+ // Return null if not found
58
+ return null
59
+ }
60
+
61
+ function findOrCreateNetworkEntity(message: utils.NetworkMessage, sender: string, isServer: boolean): Entity {
62
+ // Look for existing network entity mapping first
63
+ const existingEntity = findExistingNetworkEntity(message)
64
+
65
+ if (existingEntity) {
66
+ return existingEntity
67
+ }
68
+
69
+ // Create new entity and network mapping
70
+ const newEntityId = engine.addEntity()
71
+ NetworkEntity.createOrReplace(newEntityId, {
72
+ networkId: message.networkId,
73
+ entityId: message.entityId
74
+ })
75
+
76
+ if (isServer) {
77
+ CreatedBy.createOrReplace(newEntityId, { address: sender })
78
+ }
79
+
80
+ DEBUG_NETWORK_MESSAGES() &&
81
+ console.log(`[DEBUG] Created new entity ${newEntityId} for network ${message.networkId}:${message.entityId}`)
82
+ return newEntityId
83
+ }
84
+
85
+ function convertNetworkToRegularMessage(
86
+ networkMessage: utils.NetworkMessage,
87
+ localEntityId: Entity,
88
+ forceCorrections = false
89
+ ): (CrdtMessageBody & { messageBuffer: Uint8Array }) | null {
90
+ const buffer = new ReadWriteByteBuffer()
91
+
92
+ try {
93
+ // Use the well-tested networkMessageToLocal utility with transform fixing for Unity
94
+ const message = utils.networkMessageToLocal(
95
+ networkMessage,
96
+ localEntityId,
97
+ buffer,
98
+ NetworkParent,
99
+ forceCorrections
100
+ )
101
+ return { ...message, messageBuffer: buffer.toBinary() }
102
+ } catch (error) {
103
+ DEBUG_NETWORK_MESSAGES() && console.error('Error converting network message:', error)
104
+ return null
105
+ }
106
+ }
107
+
108
+ function validateMessagePermissions(message: utils.RegularMessage, sender: string, _localEntityId: Entity): boolean {
109
+ // Basic checks
110
+ if (!sender || sender === AUTH_SERVER_PEER_ID) {
111
+ return false // Server shouldn't send messages to itself
112
+ }
113
+
114
+ if (message.type === CrdtMessageType.DELETE_ENTITY) {
115
+ // TODO: how to handle this case ?
116
+ }
117
+
118
+ if (message.type === CrdtMessageType.PUT_COMPONENT || message.type === CrdtMessageType.DELETE_COMPONENT) {
119
+ const component = engine.getComponent(message.componentId) as InternalBaseComponent<unknown>
120
+ const buf = 'data' in message ? new ReadWriteByteBuffer(message.data) : null
121
+ const value = buf ? component.schema.deserialize(buf) : null
122
+ const dryRunCRDT = component.__dry_run_updateFromCrdt(message)
123
+ const validCRDT = [
124
+ ProcessMessageResultType.StateUpdatedData,
125
+ ProcessMessageResultType.StateUpdatedTimestamp,
126
+ ProcessMessageResultType.EntityDeleted
127
+ ].includes(dryRunCRDT)
128
+ const createdBy = CreatedBy.getOrNull(message.entityId)
129
+ const validMessage =
130
+ validCRDT &&
131
+ component.__run_validateBeforeChange(message.entityId, value, sender, createdBy?.address ?? AUTH_SERVER_PEER_ID)
132
+
133
+ return !!validMessage
134
+ }
135
+
136
+ // For now, basic validation - in the future this will check component sync permissions
137
+ // TODO: Check if sender owns the entity
138
+ // TODO: Check component sync mode ('all' | 'owner' | 'server')
139
+ // TODO: Run component custom validation
140
+ return true
141
+ }
142
+
143
+ function broadcastBatchedMessages(messages: utils.NetworkMessage[], excludeSender: string) {
144
+ if (messages.length === 0) return
145
+
146
+ // Build the complete buffer with all messages
147
+ const networkBuffer = new ReadWriteByteBuffer()
148
+ for (const message of messages) {
149
+ // Skip oversized messages upfront
150
+ if (message.messageBuffer.byteLength / 1024 > LIVEKIT_MAX_SIZE) {
151
+ console.error(
152
+ `Message too large (${message.messageBuffer.byteLength} bytes), skipping message from ${excludeSender}`
153
+ )
154
+ continue
155
+ }
156
+ networkBuffer.writeBuffer(message.messageBuffer, false)
157
+ }
158
+
159
+ // Use the chunking function to split into proper chunks
160
+ const chunks = chunkCrdtMessages(networkBuffer.toBinary(), LIVEKIT_MAX_SIZE)
161
+
162
+ for (const chunk of chunks) {
163
+ binaryMessageBus.emit(CommsMessage.CRDT, chunk)
164
+ }
165
+ DEBUG_NETWORK_MESSAGES() &&
166
+ console.log(`Total: ${messages.length} messages in ${chunks.length} chunks from ${excludeSender}`)
167
+ }
168
+
169
+ function sendCorrectionToSender(networkMessage: utils.NetworkMessage, sender: string, localEntityId: Entity) {
170
+ try {
171
+ // Only handle component messages (PUT/DELETE), not entity deletion
172
+ if (networkMessage.type === CrdtMessageType.DELETE_ENTITY_NETWORK) {
173
+ DEBUG_NETWORK_MESSAGES() && console.log('[AUTHORITATIVE] Cannot send authoritative message for entity deletion')
174
+ return
175
+ }
176
+
177
+ // Safe to access componentId and timestamp now
178
+ const component = engine.getComponent(networkMessage.componentId)
179
+
180
+ // Only proceed if component supports authoritative messages (LWW or GrowOnlySet)
181
+ if (!supportsCorrections(component)) {
182
+ DEBUG_NETWORK_MESSAGES() && console.log('[AUTHORITATIVE] Component does not support authoritative messages')
183
+ return
184
+ }
185
+
186
+ const serverCRDTState = component.getCrdtState(localEntityId)
187
+
188
+ if (serverCRDTState) {
189
+ // Create authoritative message using PUT_COMPONENT_NETWORK
190
+ // Each client will convert this to AUTHORITATIVE_PUT_COMPONENT with proper entity mapping
191
+ const correctionBuffer = new ReadWriteByteBuffer()
192
+ PutNetworkComponentOperation.write(
193
+ networkMessage.entityId, // Use original network entity ID
194
+ serverCRDTState.timestamp,
195
+ networkMessage.componentId,
196
+ networkMessage.networkId,
197
+ serverCRDTState.data,
198
+ correctionBuffer
199
+ )
200
+ // Send authoritative message directly to the sender
201
+ binaryMessageBus.emit(CommsMessage.CRDT_AUTHORITATIVE, correctionBuffer.toBinary(), [sender])
202
+
203
+ DEBUG_NETWORK_MESSAGES() &&
204
+ console.log(
205
+ `[AUTHORITATIVE] Sent authoritative message to ${sender} for entity ${localEntityId} component ${networkMessage.componentId} with timestamp ${networkMessage.timestamp}`
206
+ )
207
+ }
208
+ } catch (error) {
209
+ DEBUG_NETWORK_MESSAGES() && console.error('Error sending correction:', error)
210
+ }
211
+ }
212
+
213
+ return {
214
+ findExistingNetworkEntity,
215
+ // transform Network messages to CRDT Common Messages.
216
+ processClientMessages: function processClientMessages(value: Uint8Array, sender: string, forceCorrections = false) {
217
+ // console.log(`[CLIENT] Processing message from ${sender}, ${value.length} bytes`)
218
+
219
+ // Collect all regular messages in a single buffer for batched application
220
+ const combinedBuffer = new ReadWriteByteBuffer()
221
+
222
+ // Clients process network messages from server and convert them to regular messages
223
+ for (const message of utils.readMessages(value)) {
224
+ // Only process network messages in client message handler
225
+ if (utils.isNetworkMessage(message)) {
226
+ const networkMessage = message as utils.NetworkMessage
227
+
228
+ // Find or create network entity mapping
229
+ const localEntityId = findOrCreateNetworkEntity(networkMessage, sender, false)
230
+
231
+ // Convert network message to regular message or correction message
232
+ const regularMessage = convertNetworkToRegularMessage(networkMessage, localEntityId, forceCorrections)
233
+
234
+ if (regularMessage?.messageBuffer.byteLength) {
235
+ combinedBuffer.writeBuffer(regularMessage.messageBuffer, false)
236
+ }
237
+ }
238
+ }
239
+ return combinedBuffer.toBinary()
240
+ },
241
+ // Sever Code: process message, handle permissions, and broadcast if needed.
242
+ processServerMessages: function processServerMessages(value: Uint8Array, sender: string) {
243
+ // console.log(`[SERVER] Processing message from ${sender}, ${value.length} bytes`)
244
+
245
+ // Collect all valid messages for batched broadcasting
246
+ const messagesToBroadcast: utils.NetworkMessage[] = []
247
+ const regularMessagesBuffer = new ReadWriteByteBuffer()
248
+
249
+ for (const message of utils.readMessages(value)) {
250
+ try {
251
+ // Only process network messages in server message handler
252
+ if (utils.isNetworkMessage(message)) {
253
+ const networkMessage = message as utils.NetworkMessage
254
+ // 1. Find or create network entity mapping
255
+ const localEntityId = findOrCreateNetworkEntity(networkMessage, sender, true)
256
+
257
+ // 2. Convert network message to regular message and collect for local application
258
+ const regularMessage = convertNetworkToRegularMessage(networkMessage, localEntityId)
259
+
260
+ // 3. Basic permission validation
261
+ if (!validateMessagePermissions(regularMessage as any, sender, localEntityId)) {
262
+ // Send correction back to sender with server's authoritative state
263
+ sendCorrectionToSender(networkMessage, sender, localEntityId)
264
+ continue
265
+ }
266
+
267
+ // 4. Collect valid message for batched broadcasting
268
+ messagesToBroadcast.push(networkMessage)
269
+
270
+ if (regularMessage?.messageBuffer.byteLength) {
271
+ regularMessagesBuffer.writeBuffer(regularMessage.messageBuffer, false)
272
+ }
273
+ }
274
+ } catch (error) {
275
+ DEBUG_NETWORK_MESSAGES() && console.error('Error processing server message:', error)
276
+ }
277
+ }
278
+ // Batch broadcast all valid messages together
279
+ broadcastBatchedMessages(messagesToBroadcast, sender)
280
+ return regularMessagesBuffer.toBinary()
281
+ },
282
+ // engine changes that needs to be broadcasted.
283
+ convertRegularToNetworkMessage: function convertRegularToNetworkMessage(regularMessage: Uint8Array): Uint8Array[] {
284
+ const groupedBuffer = new ReadWriteByteBuffer()
285
+
286
+ // First pass: Convert all regular messages to network format and group them into one big buffer
287
+ for (const message of utils.readMessages(regularMessage)) {
288
+ // Only convert regular messages that have network data
289
+ const networkData = NetworkEntity.getOrNull(message.entityId)
290
+
291
+ if (networkData && !utils.isNetworkMessage(message)) {
292
+ utils.localMessageToNetwork(message, networkData, groupedBuffer)
293
+ }
294
+ }
295
+
296
+ // Second pass: Use the new chunking function that respects message boundaries
297
+ const totalData = groupedBuffer.toBinary()
298
+ return chunkCrdtMessages(totalData, LIVEKIT_MAX_SIZE)
299
+ }
300
+ }
301
+ }