@dcl/sdk 7.26.0 → 7.26.1-31714079767.commit-96e9a29

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 (69) 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 +221 -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 +16 -3
  20. package/network/message-bus-sync.js +161 -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 +79 -0
  32. package/server/storage/constants.js +19 -0
  33. package/server/storage/index.d.ts +45 -0
  34. package/server/storage/index.js +51 -0
  35. package/server/storage/player.d.ts +45 -0
  36. package/server/storage/player.js +198 -0
  37. package/server/storage/scene.d.ts +40 -0
  38. package/server/storage/scene.js +188 -0
  39. package/server/storage/value-cache.d.ts +1 -0
  40. package/server/storage/value-cache.js +52 -0
  41. package/server/storage/write-queue.d.ts +1 -0
  42. package/server/storage/write-queue.js +71 -0
  43. package/server/storage-url.d.ts +13 -0
  44. package/server/storage-url.js +42 -0
  45. package/server/utils.d.ts +35 -0
  46. package/server/utils.js +61 -0
  47. package/src/atom.ts +98 -0
  48. package/src/future.ts +38 -0
  49. package/src/network/binary-message-bus.ts +9 -4
  50. package/src/network/chunking.ts +45 -0
  51. package/src/network/events/implementation.ts +271 -0
  52. package/src/network/events/index.ts +48 -0
  53. package/src/network/events/protocol.ts +94 -0
  54. package/src/network/events/registry.ts +18 -0
  55. package/src/network/index.ts +40 -3
  56. package/src/network/message-bus-sync.ts +176 -112
  57. package/src/network/server/index.ts +301 -0
  58. package/src/network/server/utils.ts +189 -0
  59. package/src/network/state.ts +3 -4
  60. package/src/server/env-var.ts +36 -0
  61. package/src/server/index.ts +12 -0
  62. package/src/server/storage/constants.ts +111 -0
  63. package/src/server/storage/index.ts +74 -0
  64. package/src/server/storage/player.ts +289 -0
  65. package/src/server/storage/scene.ts +270 -0
  66. package/src/server/storage/value-cache.ts +92 -0
  67. package/src/server/storage/write-queue.ts +131 -0
  68. package/src/server/storage-url.ts +49 -0
  69. package/src/server/utils.ts +76 -0
@@ -0,0 +1,189 @@
1
+ import { Entity } from '@dcl/ecs/dist/engine'
2
+ import { CrdtMessageProtocol, NetworkParent } from '@dcl/ecs'
3
+ import { ReceiveMessage } from '@dcl/ecs/dist/runtime/types'
4
+ import { ReceiveNetworkMessage } from '@dcl/ecs/dist/systems/crdt/types'
5
+ import { ByteBuffer, ReadWriteByteBuffer } from '@dcl/ecs/dist/serialization/ByteBuffer'
6
+ import { AuthoritativePutComponentOperation, PutComponentOperation } from '@dcl/ecs/dist/serialization/crdt'
7
+ import {
8
+ CrdtMessage,
9
+ CrdtMessageBody,
10
+ CrdtMessageHeader,
11
+ CrdtMessageType,
12
+ DeleteComponentMessage,
13
+ DeleteComponentNetworkMessage,
14
+ DeleteEntityMessage,
15
+ DeleteEntityNetworkMessage,
16
+ PutComponentMessage,
17
+ AuthoritativePutComponentMessage,
18
+ PutNetworkComponentMessage
19
+ } from '@dcl/ecs/dist/serialization/crdt/types'
20
+ import { DeleteComponent } from '@dcl/ecs/dist/serialization/crdt/deleteComponent'
21
+ import { DeleteEntity } from '@dcl/ecs/dist/serialization/crdt/deleteEntity'
22
+ import { INetowrkEntityType } from '@dcl/ecs/dist/components/types'
23
+ import { PutNetworkComponentOperation } from '@dcl/ecs/dist/serialization/crdt/network/putComponentNetwork'
24
+ import { DeleteComponentNetwork } from '@dcl/ecs/dist/serialization/crdt/network/deleteComponentNetwork'
25
+ import { DeleteEntityNetwork } from '@dcl/ecs/dist/serialization/crdt/network/deleteEntityNetwork'
26
+ import { TransformSchema, COMPONENT_ID as TransformComponentId } from '@dcl/ecs/dist/components/manual/Transform'
27
+
28
+ export type NetworkMessage = (
29
+ | PutNetworkComponentMessage
30
+ | DeleteComponentNetworkMessage
31
+ | DeleteEntityNetworkMessage
32
+ ) & { messageBuffer: Uint8Array }
33
+
34
+ export type RegularMessage = (
35
+ | PutComponentMessage
36
+ | AuthoritativePutComponentMessage
37
+ | DeleteComponentMessage
38
+ | DeleteEntityMessage
39
+ ) & {
40
+ messageBuffer: Uint8Array
41
+ }
42
+ export function readMessages(data: Uint8Array): (NetworkMessage | RegularMessage)[] {
43
+ const buffer = new ReadWriteByteBuffer(data)
44
+ const messages: (NetworkMessage | RegularMessage)[] = []
45
+ let header: CrdtMessageHeader | null
46
+ while ((header = CrdtMessageProtocol.getHeader(buffer))) {
47
+ const offset = buffer.currentReadOffset()
48
+ let message: CrdtMessage | undefined = undefined
49
+
50
+ // Network messages
51
+ if (header.type === CrdtMessageType.DELETE_COMPONENT_NETWORK) {
52
+ message = DeleteComponentNetwork.read(buffer)!
53
+ } else if (header.type === CrdtMessageType.PUT_COMPONENT_NETWORK) {
54
+ message = PutNetworkComponentOperation.read(buffer)!
55
+ } else if (header.type === CrdtMessageType.DELETE_ENTITY_NETWORK) {
56
+ message = DeleteEntityNetwork.read(buffer)!
57
+ }
58
+ // Regular messages
59
+ else if (header.type === CrdtMessageType.PUT_COMPONENT) {
60
+ message = PutComponentOperation.read(buffer)!
61
+ } else if (header.type === CrdtMessageType.AUTHORITATIVE_PUT_COMPONENT) {
62
+ message = AuthoritativePutComponentOperation.read(buffer)!
63
+ } else if (header.type === CrdtMessageType.DELETE_COMPONENT) {
64
+ message = DeleteComponent.read(buffer)!
65
+ } else if (header.type === CrdtMessageType.DELETE_ENTITY) {
66
+ message = DeleteEntity.read(buffer)!
67
+ } else {
68
+ // consume unknown messages
69
+ buffer.incrementReadOffset(header.length)
70
+ }
71
+
72
+ if (message) {
73
+ messages.push({
74
+ ...message,
75
+ messageBuffer: buffer.buffer().subarray(offset, buffer.currentReadOffset())
76
+ })
77
+ }
78
+ }
79
+ return messages
80
+ }
81
+
82
+ export function isNetworkMessage(message: ReceiveMessage): message is ReceiveNetworkMessage {
83
+ return [
84
+ CrdtMessageType.DELETE_COMPONENT_NETWORK,
85
+ CrdtMessageType.DELETE_ENTITY_NETWORK,
86
+ CrdtMessageType.PUT_COMPONENT_NETWORK
87
+ ].includes(message.type)
88
+ }
89
+
90
+ export function networkMessageToLocal(
91
+ message: ReceiveNetworkMessage,
92
+ localEntityId: Entity,
93
+ destinationBuffer: ByteBuffer,
94
+ // Optional network parent component for transform fixing
95
+ networkParentComponent?: typeof NetworkParent,
96
+ // Force corrections - converts PUT_COMPONENT_NETWORK to authoritative_PUT_COMPONENT
97
+ forceCorrections = false
98
+ ): CrdtMessageBody {
99
+ if (message.type === CrdtMessageType.PUT_COMPONENT_NETWORK) {
100
+ let messageData = message.data
101
+
102
+ // Fix transform parent if needed for Unity/engine processing
103
+ if (message.componentId === TransformComponentId && networkParentComponent) {
104
+ const parentNetwork = networkParentComponent.getOrNull(localEntityId)
105
+ messageData = fixTransformParent(message, parentNetwork?.entityId)
106
+ }
107
+ if (forceCorrections) {
108
+ // Use AUTHORITATIVE_PUT_COMPONENT for forced state updates
109
+ AuthoritativePutComponentOperation.write(
110
+ localEntityId,
111
+ message.timestamp,
112
+ message.componentId,
113
+ messageData,
114
+ destinationBuffer
115
+ )
116
+ return {
117
+ type: CrdtMessageType.AUTHORITATIVE_PUT_COMPONENT,
118
+ componentId: message.componentId,
119
+ timestamp: message.timestamp,
120
+ data: messageData,
121
+ entityId: localEntityId
122
+ }
123
+ } else {
124
+ // Normal PUT_COMPONENT conversion
125
+ PutComponentOperation.write(localEntityId, message.timestamp, message.componentId, messageData, destinationBuffer)
126
+ return {
127
+ type: CrdtMessageType.PUT_COMPONENT,
128
+ componentId: message.componentId,
129
+ timestamp: message.timestamp,
130
+ data: messageData,
131
+ entityId: localEntityId
132
+ }
133
+ }
134
+ } else if (message.type === CrdtMessageType.DELETE_COMPONENT_NETWORK) {
135
+ DeleteComponent.write(localEntityId, message.componentId, message.timestamp, destinationBuffer)
136
+ return {
137
+ type: CrdtMessageType.DELETE_COMPONENT,
138
+ componentId: message.componentId,
139
+ timestamp: message.timestamp,
140
+ entityId: localEntityId
141
+ }
142
+ } else if (message.type === CrdtMessageType.DELETE_ENTITY_NETWORK) {
143
+ DeleteEntity.write(localEntityId, destinationBuffer)
144
+ return {
145
+ type: CrdtMessageType.DELETE_ENTITY,
146
+ entityId: localEntityId
147
+ }
148
+ }
149
+ throw 1
150
+ }
151
+
152
+ export function localMessageToNetwork(
153
+ message: ReceiveMessage,
154
+ network: INetowrkEntityType,
155
+ destinationBuffer: ByteBuffer
156
+ ) {
157
+ if (message.type === CrdtMessageType.PUT_COMPONENT) {
158
+ PutNetworkComponentOperation.write(
159
+ network.entityId,
160
+ message.timestamp,
161
+ message.componentId,
162
+ network.networkId,
163
+ message.data,
164
+ destinationBuffer
165
+ )
166
+ } else if (message.type === CrdtMessageType.DELETE_COMPONENT) {
167
+ DeleteComponentNetwork.write(
168
+ network.entityId,
169
+ message.componentId,
170
+ message.timestamp,
171
+ network.networkId,
172
+ destinationBuffer
173
+ )
174
+ } else if (message.type === CrdtMessageType.DELETE_ENTITY) {
175
+ DeleteEntityNetwork.write(network.entityId, network.networkId, destinationBuffer)
176
+ }
177
+ }
178
+
179
+ export function fixTransformParent(message: ReceiveMessage, parent?: Entity): Uint8Array {
180
+ const buffer = new ReadWriteByteBuffer()
181
+ const transform = 'data' in message && TransformSchema.deserialize(new ReadWriteByteBuffer(message.data))
182
+
183
+ if (!transform) throw new Error('Invalid parent transform')
184
+
185
+ // Generate new transform raw data with the parent
186
+ const newTransform = { ...transform, parent }
187
+ TransformSchema.serialize(newTransform, buffer)
188
+ return buffer.toBinary()
189
+ }
@@ -28,7 +28,7 @@ import {
28
28
  TriggerAreaResult,
29
29
  ComponentDefinition
30
30
  } from '@dcl/ecs'
31
- import { LIVEKIT_MAX_SIZE } from '@dcl/ecs/dist/systems/crdt'
31
+ import { LIVEKIT_MAX_SIZE } from './server'
32
32
 
33
33
  export const NOT_SYNC_COMPONENTS: ComponentDefinition<unknown>[] = [
34
34
  VideoEvent,
@@ -79,9 +79,9 @@ export function engineToCrdt(engine: IEngine): Uint8Array[] {
79
79
  if (!shouldSyncComponent(itComponentDefinition)) {
80
80
  continue
81
81
  }
82
+
82
83
  itComponentDefinition.dumpCrdtStateToBuffer(crdtBuffer, (entity) => {
83
- const isNetworkEntity = NetworkEntity.has(entity)
84
- return isNetworkEntity
84
+ return NetworkEntity.has(entity)
85
85
  })
86
86
  }
87
87
 
@@ -103,7 +103,6 @@ export function engineToCrdt(engine: IEngine): Uint8Array[] {
103
103
  }
104
104
 
105
105
  // If the message itself is larger than the limit, we need to handle it specially
106
- // For now, we'll skip it to prevent infinite loops
107
106
  if (messageSize / 1024 > LIVEKIT_MAX_SIZE) {
108
107
  console.error(
109
108
  `Message too large (${messageSize} bytes), skipping component ${message.componentId} for entity ${message.entityId}`
@@ -0,0 +1,36 @@
1
+ import { getStorageServerUrl } from './storage-url'
2
+ import { assertIsServer, wrapSignedFetch } from './utils'
3
+
4
+ const MODULE_NAME = 'EnvVar'
5
+
6
+ /**
7
+ * EnvVar provides methods to fetch environment variables from the
8
+ * Server Side Storage service. This module only works when running
9
+ * on server-side scenes.
10
+ */
11
+ export const EnvVar = {
12
+ /**
13
+ * Fetches a specific environment variable by key as plain text.
14
+ *
15
+ * @param key - The name of the environment variable to fetch
16
+ * @returns A promise that resolves to the plain text value, or empty string if not found
17
+ * @throws Error if not running on a server-side scene
18
+ */
19
+ async get(key: string): Promise<string> {
20
+ assertIsServer(MODULE_NAME)
21
+
22
+ const baseUrl = await getStorageServerUrl()
23
+ const url = `${baseUrl}/env/${encodeURIComponent(key)}`
24
+
25
+ const [error, data] = await wrapSignedFetch<{ value: string }>({
26
+ url
27
+ })
28
+
29
+ if (error) {
30
+ console.error(`Failed to fetch environment variable '${key}': ${error}`)
31
+ return ''
32
+ }
33
+
34
+ return data?.value ?? ''
35
+ }
36
+ }
@@ -0,0 +1,12 @@
1
+ export { EnvVar } from './env-var'
2
+ export {
3
+ Storage,
4
+ IStorage,
5
+ ISceneStorage,
6
+ IPlayerStorage,
7
+ GetOptions,
8
+ GetValuesOptions,
9
+ GetValuesResult,
10
+ SetOptions,
11
+ StorageOptions
12
+ } from './storage'
@@ -0,0 +1,111 @@
1
+ export const MODULE_NAME = 'Storage'
2
+
3
+ /**
4
+ * Options for getValues pagination and filtering.
5
+ */
6
+ export interface GetValuesOptions {
7
+ prefix?: string
8
+ limit?: number
9
+ offset?: number
10
+ }
11
+
12
+ /**
13
+ * Result of getValues with pagination metadata.
14
+ */
15
+ export interface GetValuesResult {
16
+ /** Key-value entries for the current page. */
17
+ data: Array<{ key: string; value: unknown }>
18
+ pagination: {
19
+ offset: number
20
+ total: number
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Per-call options for Storage get().
26
+ */
27
+ export interface GetOptions {
28
+ /**
29
+ * When true, bypasses the read cache and forces a network read (the result
30
+ * still refreshes the cache). Concurrent gets for the same key still share
31
+ * one in-flight request. Default: false.
32
+ */
33
+ fresh?: boolean
34
+ }
35
+
36
+ /**
37
+ * Per-call options for Storage set().
38
+ */
39
+ export interface SetOptions {
40
+ /**
41
+ * When true, skips the network write if the serialized value matches the
42
+ * last value known to be stored for this key (from a previous successful
43
+ * set() or get()). Overrides the configured default. Default: true.
44
+ */
45
+ skipIfUnchanged?: boolean
46
+ }
47
+
48
+ /**
49
+ * Module-wide configuration for Storage, applied to both scene-scoped and
50
+ * player-scoped storage via Storage.configure().
51
+ */
52
+ export interface StorageOptions {
53
+ /**
54
+ * Default for set()'s skipIfUnchanged when not passed per call. A skip only
55
+ * suppresses a proven no-op: the exact serialized value was confirmed stored
56
+ * by a previous network round-trip within cacheMaxAgeMs. Default: true.
57
+ */
58
+ skipIfUnchanged?: boolean
59
+ /**
60
+ * When true, get() serves values from the local cache within cacheMaxAgeMs,
61
+ * including confirmed "not found" results, without a network request.
62
+ * Out-of-band writers (e.g. CLI storage commands) may not be visible for up
63
+ * to cacheMaxAgeMs; pass { fresh: true } per call to force a network read.
64
+ * Default: true.
65
+ */
66
+ cacheReads?: boolean
67
+ /**
68
+ * Max cache entries per scope (scene / player) before oldest entries are
69
+ * evicted. Entries hold the serialized value body, so memory scales with
70
+ * value size — this bounds entry count, not bytes. Default: 512.
71
+ */
72
+ cacheMaxEntries?: number
73
+ /**
74
+ * Max age of a cache entry in milliseconds, bounding both read-cache
75
+ * staleness and write-dedup trust; older entries are treated as unknown.
76
+ * The default is deliberately short so out-of-band writers (CLI storage
77
+ * commands, dashboards) become visible within a minute, while still
78
+ * absorbing hot per-frame reads. Default: 60000 (1 minute).
79
+ */
80
+ cacheMaxAgeMs?: number
81
+ }
82
+
83
+ /**
84
+ * Fully-resolved storage configuration shared by scene and player storage.
85
+ * @internal
86
+ */
87
+ export interface StorageConfigState {
88
+ skipIfUnchanged: boolean
89
+ cacheReads: boolean
90
+ cacheMaxEntries: number
91
+ cacheMaxAgeMs: number
92
+ }
93
+
94
+ /**
95
+ * Default values for the resolved storage configuration.
96
+ * @internal
97
+ */
98
+ export const DEFAULT_STORAGE_CONFIG: Readonly<StorageConfigState> = {
99
+ skipIfUnchanged: true,
100
+ cacheReads: true,
101
+ cacheMaxEntries: 512,
102
+ cacheMaxAgeMs: 60 * 1000
103
+ }
104
+
105
+ /**
106
+ * Creates a mutable config object, later mutated by Storage.configure().
107
+ * @internal
108
+ */
109
+ export function createStorageConfig(overrides?: StorageOptions): StorageConfigState {
110
+ return { ...DEFAULT_STORAGE_CONFIG, ...overrides }
111
+ }
@@ -0,0 +1,74 @@
1
+ import { createStorageConfig, StorageOptions } from './constants'
2
+ import { createSceneStorage, ISceneStorage } from './scene'
3
+ import { createPlayerStorage, IPlayerStorage } from './player'
4
+
5
+ // Re-export interfaces and types
6
+ export { GetOptions, GetValuesOptions, GetValuesResult, SetOptions, StorageOptions } from './constants'
7
+ export { ISceneStorage } from './scene'
8
+ export { IPlayerStorage } from './player'
9
+
10
+ /**
11
+ * Storage interface with methods for scene-scoped and player-scoped storage.
12
+ */
13
+ export interface IStorage extends ISceneStorage {
14
+ /** Player-scoped storage for key-value pairs */
15
+ player: IPlayerStorage
16
+
17
+ /**
18
+ * Sets module-wide defaults for both scene-scoped and player-scoped storage.
19
+ * Merges the given partial options into the current configuration.
20
+ * Note: cacheMaxEntries applies per scope (scene and player each keep their
21
+ * own cache) and bounds entry count, not bytes (entries hold serialized
22
+ * value bodies).
23
+ */
24
+ configure(options: StorageOptions): void
25
+ }
26
+
27
+ /**
28
+ * Creates the Storage module with scene-scoped and player-scoped storage.
29
+ */
30
+ const createStorage = (): IStorage => {
31
+ const config = createStorageConfig()
32
+ const sceneStorage = createSceneStorage(config)
33
+ const playerStorage = createPlayerStorage(config)
34
+
35
+ return {
36
+ // Spread scene storage methods at top level
37
+ get: sceneStorage.get,
38
+ set: sceneStorage.set,
39
+ delete: sceneStorage.delete,
40
+ getValues: sceneStorage.getValues,
41
+ // Keep player as nested property
42
+ player: playerStorage,
43
+ configure(options: StorageOptions): void {
44
+ for (const [key, value] of Object.entries(options)) {
45
+ if (value !== undefined) (config as unknown as Record<string, unknown>)[key] = value
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Storage provides methods to store and retrieve key-value data from the
53
+ * Server Side Storage service.
54
+ *
55
+ * - Use Storage.get/set/delete/getValues for scene-scoped storage
56
+ * - Use Storage.player.get/set/delete/getValues for player-scoped storage
57
+ * - Use Storage.configure to change module-wide defaults (e.g. skipIfUnchanged, cacheReads)
58
+ *
59
+ * Reads are cached by default: get() serves values known from a network
60
+ * round-trip within the last cacheMaxAgeMs (including confirmed "not found"
61
+ * results) without hitting the service, and concurrent gets for the same key
62
+ * share one request. Overlapping writes to the same key are serialized so the
63
+ * service commits them in issue order, and rapid writes coalesce to the
64
+ * latest value; every set()/delete() resolves once its value (or the newer
65
+ * value that superseded it) is durably applied. By default (skipIfUnchanged)
66
+ * an unchanged set is skipped, but only when a previous confirmed round-trip
67
+ * proved the exact value is already stored. Out-of-band writers (e.g. CLI storage commands)
68
+ * may not be visible for up to cacheMaxAgeMs; use get(key, { fresh: true })
69
+ * for an authoritative read or Storage.configure({ cacheReads: false }) to
70
+ * disable read caching.
71
+ *
72
+ * This module only works when running on server-side scenes.
73
+ */
74
+ export const Storage: IStorage = createStorage()