@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
@@ -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
+ }
@@ -25,7 +25,7 @@ import {
25
25
  UiTransform,
26
26
  ComponentDefinition
27
27
  } from '@dcl/ecs'
28
- import { LIVEKIT_MAX_SIZE } from '@dcl/ecs/dist/systems/crdt'
28
+ import { LIVEKIT_MAX_SIZE } from './server'
29
29
 
30
30
  export const NOT_SYNC_COMPONENTS: ComponentDefinition<unknown>[] = [
31
31
  VideoEvent,
@@ -72,9 +72,9 @@ export function engineToCrdt(engine: IEngine): Uint8Array[] {
72
72
  if (!shouldSyncComponent(itComponentDefinition)) {
73
73
  continue
74
74
  }
75
+
75
76
  itComponentDefinition.dumpCrdtStateToBuffer(crdtBuffer, (entity) => {
76
- const isNetworkEntity = NetworkEntity.has(entity)
77
- return isNetworkEntity
77
+ return NetworkEntity.has(entity)
78
78
  })
79
79
  }
80
80
 
@@ -96,7 +96,6 @@ export function engineToCrdt(engine: IEngine): Uint8Array[] {
96
96
  }
97
97
 
98
98
  // If the message itself is larger than the limit, we need to handle it specially
99
- // For now, we'll skip it to prevent infinite loops
100
99
  if (messageSize / 1024 > LIVEKIT_MAX_SIZE) {
101
100
  console.error(
102
101
  `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,2 @@
1
+ export { EnvVar } from './env-var'
2
+ export { Storage, IStorage, ISceneStorage, IPlayerStorage, GetValuesOptions, GetValuesResult } from './storage'
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,44 @@
1
+ import { createSceneStorage, ISceneStorage } from './scene'
2
+ import { createPlayerStorage, IPlayerStorage } from './player'
3
+
4
+ // Re-export interfaces and types
5
+ export { GetValuesOptions, GetValuesResult } from './constants'
6
+ export { ISceneStorage } from './scene'
7
+ export { IPlayerStorage } from './player'
8
+
9
+ /**
10
+ * Storage interface with methods for scene-scoped and player-scoped storage.
11
+ */
12
+ export interface IStorage extends ISceneStorage {
13
+ /** Player-scoped storage for key-value pairs */
14
+ player: IPlayerStorage
15
+ }
16
+
17
+ /**
18
+ * Creates the Storage module with scene-scoped and player-scoped storage.
19
+ */
20
+ const createStorage = (): IStorage => {
21
+ const sceneStorage = createSceneStorage()
22
+ const playerStorage = createPlayerStorage()
23
+
24
+ return {
25
+ // Spread scene storage methods at top level
26
+ get: sceneStorage.get,
27
+ set: sceneStorage.set,
28
+ delete: sceneStorage.delete,
29
+ getValues: sceneStorage.getValues,
30
+ // Keep player as nested property
31
+ player: playerStorage
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Storage provides methods to store and retrieve key-value data from the
37
+ * Server Side Storage service.
38
+ *
39
+ * - Use Storage.get/set/delete/getValues for scene-scoped storage
40
+ * - Use Storage.player.get/set/delete/getValues for player-scoped storage
41
+ *
42
+ * This module only works when running on server-side scenes.
43
+ */
44
+ export const Storage: IStorage = createStorage()
@@ -0,0 +1,156 @@
1
+ import { getStorageServerUrl } from '../storage-url'
2
+ import { assertIsServer, wrapSignedFetch } from '../utils'
3
+ import { GetValuesOptions, GetValuesResult, MODULE_NAME } from './constants'
4
+
5
+ /**
6
+ * Player-scoped storage interface for key-value pairs from the Server Side Storage service.
7
+ * This is NOT filesystem storage - data is stored in the remote storage service.
8
+ */
9
+ export interface IPlayerStorage {
10
+ /**
11
+ * Retrieves a value from a player's storage by key from the Server Side Storage service.
12
+ * @param address - The player's wallet address
13
+ * @param key - The key to retrieve
14
+ * @returns A promise that resolves to the parsed JSON value, or null if not found
15
+ */
16
+ get<T = unknown>(address: string, key: string): Promise<T | null>
17
+
18
+ /**
19
+ * Stores a value in a player's storage in the Server Side Storage service.
20
+ * @param address - The player's wallet address
21
+ * @param key - The key to store the value under
22
+ * @param value - The value to store (will be JSON serialized)
23
+ * @returns A promise that resolves to true if successful, false otherwise
24
+ */
25
+ set<T = unknown>(address: string, key: string, value: T): Promise<boolean>
26
+
27
+ /**
28
+ * Deletes a value from a player's storage in the Server Side Storage service.
29
+ * @param address - The player's wallet address
30
+ * @param key - The key to delete
31
+ * @returns A promise that resolves to true if deleted, false if not found
32
+ */
33
+ delete(address: string, key: string): Promise<boolean>
34
+
35
+ /**
36
+ * Returns key-value entries from a player's storage, optionally filtered by prefix.
37
+ * Supports pagination via limit and offset.
38
+ * @param address - The player's wallet address
39
+ * @param options - Optional { prefix, limit, offset } for filtering and pagination.
40
+ * @returns A promise that resolves to { data, pagination: { offset, total } } for pagination UI
41
+ */
42
+ getValues(address: string, options?: GetValuesOptions): Promise<GetValuesResult>
43
+ }
44
+
45
+ /**
46
+ * Creates player-scoped storage that provides methods to interact with
47
+ * player-specific key-value pairs from the Server Side Storage service.
48
+ * This module only works when running on server-side scenes.
49
+ */
50
+ export const createPlayerStorage = (): IPlayerStorage => {
51
+ return {
52
+ async get<T = unknown>(address: string, key: string): Promise<T | null> {
53
+ assertIsServer(MODULE_NAME)
54
+
55
+ const baseUrl = await getStorageServerUrl()
56
+ const url = `${baseUrl}/players/${encodeURIComponent(address)}/values/${encodeURIComponent(key)}`
57
+
58
+ const [error, data] = await wrapSignedFetch<{ value: T }>({ url })
59
+
60
+ if (error) {
61
+ console.error(`Failed to get player storage value '${key}' for '${address}': ${error}`)
62
+ return null
63
+ }
64
+
65
+ return data?.value ?? null
66
+ },
67
+
68
+ async set<T = unknown>(address: string, key: string, value: T): Promise<boolean> {
69
+ assertIsServer(MODULE_NAME)
70
+
71
+ const baseUrl = await getStorageServerUrl()
72
+ const url = `${baseUrl}/players/${encodeURIComponent(address)}/values/${encodeURIComponent(key)}`
73
+
74
+ const [error] = await wrapSignedFetch({
75
+ url,
76
+ init: {
77
+ method: 'PUT',
78
+ headers: {
79
+ 'content-type': 'application/json'
80
+ },
81
+ body: JSON.stringify({ value })
82
+ }
83
+ })
84
+
85
+ if (error) {
86
+ console.error(`Failed to set player storage value '${key}' for '${address}': ${error}`)
87
+ return false
88
+ }
89
+
90
+ return true
91
+ },
92
+
93
+ async delete(address: string, key: string): Promise<boolean> {
94
+ assertIsServer(MODULE_NAME)
95
+
96
+ const baseUrl = await getStorageServerUrl()
97
+ const url = `${baseUrl}/players/${encodeURIComponent(address)}/values/${encodeURIComponent(key)}`
98
+
99
+ const [error] = await wrapSignedFetch({
100
+ url,
101
+ init: {
102
+ method: 'DELETE',
103
+ headers: {}
104
+ }
105
+ })
106
+
107
+ if (error) {
108
+ console.error(`Failed to delete player storage value '${key}' for '${address}': ${error}`)
109
+ return false
110
+ }
111
+
112
+ return true
113
+ },
114
+
115
+ async getValues(address: string, options?: GetValuesOptions): Promise<GetValuesResult> {
116
+ assertIsServer(MODULE_NAME)
117
+
118
+ const { prefix, limit, offset } = options ?? {}
119
+ const baseUrl = await getStorageServerUrl()
120
+ const parts: string[] = []
121
+
122
+ if (!!prefix) {
123
+ parts.push(`prefix=${encodeURIComponent(prefix)}`)
124
+ }
125
+
126
+ if (!!limit) {
127
+ parts.push(`limit=${limit}`)
128
+ }
129
+
130
+ if (!!offset) {
131
+ parts.push(`offset=${offset}`)
132
+ }
133
+
134
+ const query = parts.join('&')
135
+ const url = query
136
+ ? `${baseUrl}/players/${encodeURIComponent(address)}/values?${query}`
137
+ : `${baseUrl}/players/${encodeURIComponent(address)}/values`
138
+
139
+ const [error, response] = await wrapSignedFetch<GetValuesResult>({ url })
140
+
141
+ if (error) {
142
+ console.error(`Failed to get player storage values for '${address}': ${error}`)
143
+ return { data: [], pagination: { offset: 0, total: 0 } }
144
+ }
145
+
146
+ const data = response?.data ?? []
147
+ const requestedOffset = offset ?? 0
148
+ const pagination = {
149
+ offset: response?.pagination?.offset ?? requestedOffset,
150
+ total: response?.pagination?.total ?? data.length
151
+ }
152
+
153
+ return { data, pagination }
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,149 @@
1
+ import { getStorageServerUrl } from '../storage-url'
2
+ import { assertIsServer, wrapSignedFetch } from '../utils'
3
+ import { GetValuesOptions, GetValuesResult, MODULE_NAME } from './constants'
4
+
5
+ /**
6
+ * Scene-scoped storage interface for key-value pairs from the Server Side Storage service.
7
+ * This is NOT filesystem storage - data is stored in the remote storage service.
8
+ */
9
+ export interface ISceneStorage {
10
+ /**
11
+ * Retrieves a value from scene storage by key from the Server Side Storage service.
12
+ * @param key - The key to retrieve
13
+ * @returns A promise that resolves to the parsed JSON value, or null if not found
14
+ */
15
+ get<T = unknown>(key: string): Promise<T | null>
16
+
17
+ /**
18
+ * Stores a value in scene storage in the Server Side Storage service.
19
+ * @param key - The key to store the value under
20
+ * @param value - The value to store (will be JSON serialized)
21
+ */
22
+ set<T = unknown>(key: string, value: T): Promise<boolean>
23
+
24
+ /**
25
+ * Deletes a value from scene storage in the Server Side Storage service.
26
+ * @param key - The key to delete
27
+ * @returns A promise that resolves to true if deleted, false if not found
28
+ */
29
+ delete(key: string): Promise<boolean>
30
+
31
+ /**
32
+ * Returns key-value entries from scene storage, optionally filtered by prefix.
33
+ * Supports pagination via limit and offset.
34
+ * @param options - Optional { prefix, limit, offset } for filtering and pagination.
35
+ * @returns A promise that resolves to { data, pagination: { offset, total } } for pagination UI
36
+ */
37
+ getValues(options?: GetValuesOptions): Promise<GetValuesResult>
38
+ }
39
+
40
+ /**
41
+ * Creates scene-scoped storage that provides methods to interact with
42
+ * scene-specific key-value pairs from the Server Side Storage service.
43
+ * This module only works when running on server-side scenes.
44
+ */
45
+ export const createSceneStorage = (): ISceneStorage => {
46
+ return {
47
+ async get<T = unknown>(key: string): Promise<T | null> {
48
+ assertIsServer(MODULE_NAME)
49
+
50
+ const baseUrl = await getStorageServerUrl()
51
+ const url = `${baseUrl}/values/${encodeURIComponent(key)}`
52
+
53
+ const [error, data] = await wrapSignedFetch<{ value: T }>({ url })
54
+
55
+ if (error) {
56
+ console.error(`Failed to get storage value '${key}': ${error}`)
57
+ return null
58
+ }
59
+
60
+ return data?.value ?? null
61
+ },
62
+
63
+ async set<T = unknown>(key: string, value: T): Promise<boolean> {
64
+ assertIsServer(MODULE_NAME)
65
+
66
+ const baseUrl = await getStorageServerUrl()
67
+ const url = `${baseUrl}/values/${encodeURIComponent(key)}`
68
+
69
+ const [error] = await wrapSignedFetch({
70
+ url,
71
+ init: {
72
+ method: 'PUT',
73
+ headers: {
74
+ 'content-type': 'application/json'
75
+ },
76
+ body: JSON.stringify({ value })
77
+ }
78
+ })
79
+
80
+ if (error) {
81
+ console.error(`Failed to set storage value '${key}': ${error}`)
82
+ return false
83
+ }
84
+
85
+ return true
86
+ },
87
+
88
+ async delete(key: string): Promise<boolean> {
89
+ assertIsServer(MODULE_NAME)
90
+
91
+ const baseUrl = await getStorageServerUrl()
92
+ const url = `${baseUrl}/values/${encodeURIComponent(key)}`
93
+
94
+ const [error] = await wrapSignedFetch({
95
+ url,
96
+ init: {
97
+ method: 'DELETE',
98
+ headers: {}
99
+ }
100
+ })
101
+
102
+ if (error) {
103
+ console.error(`Failed to delete storage value '${key}': ${error}`)
104
+ return false
105
+ }
106
+
107
+ return true
108
+ },
109
+
110
+ async getValues(options?: GetValuesOptions): Promise<GetValuesResult> {
111
+ assertIsServer(MODULE_NAME)
112
+
113
+ const { prefix, limit, offset } = options ?? {}
114
+ const baseUrl = await getStorageServerUrl()
115
+ const parts: string[] = []
116
+
117
+ if (!!prefix) {
118
+ parts.push(`prefix=${encodeURIComponent(prefix)}`)
119
+ }
120
+
121
+ if (!!limit) {
122
+ parts.push(`limit=${limit}`)
123
+ }
124
+
125
+ if (!!offset) {
126
+ parts.push(`offset=${offset}`)
127
+ }
128
+
129
+ const query = parts.join('&')
130
+ const url = query ? `${baseUrl}/values?${query}` : `${baseUrl}/values`
131
+
132
+ const [error, response] = await wrapSignedFetch<GetValuesResult>({ url })
133
+
134
+ if (error) {
135
+ console.error(`Failed to get storage values: ${error}`)
136
+ return { data: [], pagination: { offset: 0, total: 0 } }
137
+ }
138
+
139
+ const data = response?.data ?? []
140
+ const requestedOffset = offset ?? 0
141
+ const pagination = {
142
+ offset: response?.pagination?.offset ?? requestedOffset,
143
+ total: response?.pagination?.total ?? data.length
144
+ }
145
+
146
+ return { data, pagination }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,34 @@
1
+ import { getRealm } from '~system/Runtime'
2
+
3
+ const STORAGE_SERVER_ORG = 'https://storage.decentraland.org'
4
+ const STORAGE_SERVER_ZONE = 'https://storage.decentraland.zone'
5
+
6
+ /**
7
+ * Determines the correct storage server URL based on the current realm.
8
+ *
9
+ * - If `isPreview` is true, uses the realm's baseUrl (localhost)
10
+ * - If the realm's baseUrl contains `.zone`, uses storage.decentraland.zone
11
+ * - Otherwise, uses storage.decentraland.org (production)
12
+ *
13
+ * @returns The storage server base URL
14
+ */
15
+ export async function getStorageServerUrl(): Promise<string> {
16
+ const { realmInfo } = await getRealm({})
17
+
18
+ if (!realmInfo) {
19
+ throw new Error('Unable to retrieve realm information')
20
+ }
21
+
22
+ // Local development / preview mode
23
+ if (realmInfo.isPreview) {
24
+ return realmInfo.baseUrl
25
+ }
26
+
27
+ // Staging / testing environment
28
+ if (realmInfo.baseUrl.includes('.zone')) {
29
+ return STORAGE_SERVER_ZONE
30
+ }
31
+
32
+ // Production environment
33
+ return STORAGE_SERVER_ORG
34
+ }