@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.
- package/atom.d.ts +19 -0
- package/atom.js +83 -0
- package/future.d.ts +8 -0
- package/future.js +26 -0
- package/network/binary-message-bus.d.ts +6 -3
- package/network/binary-message-bus.js +9 -5
- package/network/chunking.d.ts +5 -0
- package/network/chunking.js +38 -0
- package/network/events/implementation.d.ts +93 -0
- package/network/events/implementation.js +230 -0
- package/network/events/index.d.ts +42 -0
- package/network/events/index.js +43 -0
- package/network/events/protocol.d.ts +27 -0
- package/network/events/protocol.js +66 -0
- package/network/events/registry.d.ts +8 -0
- package/network/events/registry.js +3 -0
- package/network/index.d.ts +8 -2
- package/network/index.js +16 -3
- package/network/message-bus-sync.d.ts +14 -1
- package/network/message-bus-sync.js +166 -103
- package/network/server/index.d.ts +14 -0
- package/network/server/index.js +219 -0
- package/network/server/utils.d.ts +18 -0
- package/network/server/utils.js +135 -0
- package/network/state.js +3 -5
- package/package.json +6 -6
- package/server/env-var.d.ts +15 -0
- package/server/env-var.js +31 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +3 -0
- package/server/storage/constants.d.ts +23 -0
- package/server/storage/constants.js +2 -0
- package/server/storage/index.d.ts +22 -0
- package/server/storage/index.js +29 -0
- package/server/storage/player.d.ts +43 -0
- package/server/storage/player.js +92 -0
- package/server/storage/scene.d.ts +38 -0
- package/server/storage/scene.js +90 -0
- package/server/storage-url.d.ts +10 -0
- package/server/storage-url.js +29 -0
- package/server/utils.d.ts +35 -0
- package/server/utils.js +56 -0
- package/src/atom.ts +98 -0
- package/src/future.ts +38 -0
- package/src/network/binary-message-bus.ts +9 -4
- package/src/network/chunking.ts +45 -0
- package/src/network/events/implementation.ts +286 -0
- package/src/network/events/index.ts +48 -0
- package/src/network/events/protocol.ts +94 -0
- package/src/network/events/registry.ts +18 -0
- package/src/network/index.ts +40 -3
- package/src/network/message-bus-sync.ts +180 -110
- package/src/network/server/index.ts +301 -0
- package/src/network/server/utils.ts +189 -0
- package/src/network/state.ts +3 -4
- package/src/server/env-var.ts +36 -0
- package/src/server/index.ts +2 -0
- package/src/server/storage/constants.ts +22 -0
- package/src/server/storage/index.ts +44 -0
- package/src/server/storage/player.ts +156 -0
- package/src/server/storage/scene.ts +149 -0
- package/src/server/storage-url.ts +34 -0
- 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
|
+
}
|
package/src/network/state.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
UiTransform,
|
|
26
26
|
ComponentDefinition
|
|
27
27
|
} from '@dcl/ecs'
|
|
28
|
-
import { LIVEKIT_MAX_SIZE } from '
|
|
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
|
-
|
|
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,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
|
+
}
|