@dcl/sdk 7.30.1 → 7.30.2-37807847179.commit-24c42ea
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/internal/transports/rendererTransport.js +5 -2
- 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 +221 -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 +16 -3
- package/network/message-bus-sync.js +164 -103
- package/network/server/index.d.ts +14 -0
- package/network/server/index.js +256 -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 +80 -0
- package/server/storage/constants.js +19 -0
- package/server/storage/error-message.d.ts +1 -0
- package/server/storage/error-message.js +9 -0
- package/server/storage/index.d.ts +54 -0
- package/server/storage/index.js +60 -0
- package/server/storage/player.d.ts +60 -0
- package/server/storage/player.js +244 -0
- package/server/storage/scene.d.ts +55 -0
- package/server/storage/scene.js +232 -0
- package/server/storage/serialize.d.ts +1 -0
- package/server/storage/serialize.js +135 -0
- package/server/storage/value-cache.d.ts +1 -0
- package/server/storage/value-cache.js +67 -0
- package/server/storage/write-queue.d.ts +1 -0
- package/server/storage/write-queue.js +113 -0
- package/server/storage-url.d.ts +13 -0
- package/server/storage-url.js +42 -0
- package/server/utils.d.ts +35 -0
- package/server/utils.js +69 -0
- package/src/atom.ts +98 -0
- package/src/future.ts +38 -0
- package/src/internal/transports/rendererTransport.ts +4 -1
- package/src/network/binary-message-bus.ts +9 -4
- package/src/network/chunking.ts +45 -0
- package/src/network/events/implementation.ts +271 -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 -112
- package/src/network/server/index.ts +341 -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 +12 -0
- package/src/server/storage/constants.ts +112 -0
- package/src/server/storage/error-message.ts +8 -0
- package/src/server/storage/index.ts +83 -0
- package/src/server/storage/player.ts +345 -0
- package/src/server/storage/scene.ts +324 -0
- package/src/server/storage/serialize.ts +132 -0
- package/src/server/storage/value-cache.ts +119 -0
- package/src/server/storage/write-queue.ts +174 -0
- package/src/server/storage-url.ts +49 -0
- package/src/server/utils.ts +83 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rejection of a superseded delete when the write that replaced it did not apply.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export class SupersedingWriteFailed extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('a write that replaced it did not apply')
|
|
8
|
+
this.name = 'SupersedingWriteFailed'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A pending write operation. `body` is the serialized PUT payload, or null
|
|
14
|
+
* for a DELETE. Callers that join the op share its promise.
|
|
15
|
+
*/
|
|
16
|
+
interface PendingOp {
|
|
17
|
+
body: string | null
|
|
18
|
+
execute: (body: string | null) => Promise<boolean>
|
|
19
|
+
promise: Promise<boolean>
|
|
20
|
+
resolve: (result: boolean) => void
|
|
21
|
+
reject: (error: unknown) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface KeyState {
|
|
25
|
+
/** The op currently on the network. */
|
|
26
|
+
active: PendingOp
|
|
27
|
+
/** At most one queued op; a later write replaces it (latest wins). */
|
|
28
|
+
queued?: PendingOp
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serializes writes per key so the service commits them in issue order.
|
|
33
|
+
* Overlapping PUTs from the single scene server would otherwise race: the
|
|
34
|
+
* server keeps whichever request it processes last, while the local cache
|
|
35
|
+
* keeps whichever response arrives last — either can disagree with the last
|
|
36
|
+
* set() issued. With at most one in-flight op per key and a single queued
|
|
37
|
+
* "latest value" slot, the server's final state always matches the last
|
|
38
|
+
* write issued, and N rapid writes collapse into at most 2 network calls.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export interface WriteQueue {
|
|
42
|
+
/**
|
|
43
|
+
* Body of the latest issued write for the key (the queued op if present,
|
|
44
|
+
* else the in-flight one): a string for a PUT, null for a DELETE,
|
|
45
|
+
* undefined when no write is pending.
|
|
46
|
+
*/
|
|
47
|
+
pending(key: string): string | null | undefined
|
|
48
|
+
/** True while any write for the key is in flight or queued. */
|
|
49
|
+
isPending(key: string): boolean
|
|
50
|
+
/**
|
|
51
|
+
* Resolves once the in-flight op settles and, if one was queued at call time,
|
|
52
|
+
* the op that runs next. At most two, so writes cannot starve a waiter. Never rejects.
|
|
53
|
+
*/
|
|
54
|
+
settled(key: string): Promise<void>
|
|
55
|
+
/**
|
|
56
|
+
* Issues a write, queued behind any in-flight one and replacing any queued one
|
|
57
|
+
* (see settleSuperseded). An identical queued op is joined; `joinActive` also
|
|
58
|
+
* joins an identical in-flight op, which only dedup-tolerant callers may do.
|
|
59
|
+
* Rejects with the executor's error; see settleSuperseded for replaced ops.
|
|
60
|
+
*/
|
|
61
|
+
enqueue(
|
|
62
|
+
key: string,
|
|
63
|
+
body: string | null,
|
|
64
|
+
execute: (body: string | null) => Promise<boolean>,
|
|
65
|
+
joinActive: boolean
|
|
66
|
+
): Promise<boolean>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates the per-key write serializer shared by a storage scope.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export function createWriteQueue(): WriteQueue {
|
|
74
|
+
const keys = new Map<string, KeyState>()
|
|
75
|
+
|
|
76
|
+
function makeOp(body: string | null, execute: PendingOp['execute']): PendingOp {
|
|
77
|
+
let resolve!: (result: boolean) => void
|
|
78
|
+
let reject!: (error: unknown) => void
|
|
79
|
+
const promise = new Promise<boolean>((res, rej) => {
|
|
80
|
+
resolve = res
|
|
81
|
+
reject = rej
|
|
82
|
+
})
|
|
83
|
+
return { body, execute, promise, resolve, reject }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A replaced op resolves true when its replacement lands. On failure a set
|
|
88
|
+
* resolves false and a delete rejects, in its own terms.
|
|
89
|
+
*/
|
|
90
|
+
function settleSuperseded(superseded: PendingOp, by: PendingOp): void {
|
|
91
|
+
by.promise.then(
|
|
92
|
+
(result) => {
|
|
93
|
+
const applied = by.body === null || result
|
|
94
|
+
if (superseded.body !== null) superseded.resolve(applied)
|
|
95
|
+
else if (applied) superseded.resolve(true)
|
|
96
|
+
else superseded.reject(new SupersedingWriteFailed())
|
|
97
|
+
},
|
|
98
|
+
(error) => {
|
|
99
|
+
if (superseded.body === null) superseded.reject(error)
|
|
100
|
+
else superseded.resolve(false)
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function drain(key: string, state: KeyState): Promise<void> {
|
|
106
|
+
for (;;) {
|
|
107
|
+
const op = state.active
|
|
108
|
+
try {
|
|
109
|
+
op.resolve(await op.execute(op.body))
|
|
110
|
+
} catch (error) {
|
|
111
|
+
op.reject(error)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (state.queued) {
|
|
115
|
+
state.active = state.queued
|
|
116
|
+
state.queued = undefined
|
|
117
|
+
} else {
|
|
118
|
+
keys.delete(key)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
pending(key: string): string | null | undefined {
|
|
126
|
+
const state = keys.get(key)
|
|
127
|
+
if (!state) return undefined
|
|
128
|
+
return (state.queued ?? state.active).body
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
isPending(key: string): boolean {
|
|
132
|
+
return keys.has(key)
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async settled(key: string): Promise<void> {
|
|
136
|
+
const state = keys.get(key)
|
|
137
|
+
if (!state) return
|
|
138
|
+
const hadQueued = state.queued !== undefined
|
|
139
|
+
await state.active.promise.catch(() => undefined)
|
|
140
|
+
// drain() promotes the queued op (or its replacement) in place before this resumes.
|
|
141
|
+
if (hadQueued) await state.active.promise.catch(() => undefined)
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
enqueue(key: string, body: string | null, execute: PendingOp['execute'], joinActive: boolean): Promise<boolean> {
|
|
145
|
+
const state = keys.get(key)
|
|
146
|
+
|
|
147
|
+
if (!state) {
|
|
148
|
+
const op = makeOp(body, execute)
|
|
149
|
+
const newState: KeyState = { active: op }
|
|
150
|
+
keys.set(key, newState)
|
|
151
|
+
void drain(key, newState)
|
|
152
|
+
return op.promise
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (state.queued) {
|
|
156
|
+
// A queued op has not started, so it is issued "after" this caller
|
|
157
|
+
// either way: join it when identical, supersede it otherwise.
|
|
158
|
+
if (state.queued.body === body) return state.queued.promise
|
|
159
|
+
const op = makeOp(body, execute)
|
|
160
|
+
settleSuperseded(state.queued, op)
|
|
161
|
+
state.queued = op
|
|
162
|
+
return op.promise
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (joinActive && state.active.body === body) {
|
|
166
|
+
return state.active.promise
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const op = makeOp(body, execute)
|
|
170
|
+
state.queued = op
|
|
171
|
+
return op.promise
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
async function resolveStorageServerUrl(): Promise<string> {
|
|
7
|
+
const { realmInfo } = await getRealm({})
|
|
8
|
+
|
|
9
|
+
if (!realmInfo) {
|
|
10
|
+
throw new Error('Unable to retrieve realm information')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Local development / preview mode
|
|
14
|
+
if (realmInfo.isPreview) {
|
|
15
|
+
return realmInfo.baseUrl
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Staging / testing environment
|
|
19
|
+
if (realmInfo.baseUrl.includes('.zone')) {
|
|
20
|
+
return STORAGE_SERVER_ZONE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Production environment
|
|
24
|
+
return STORAGE_SERVER_ORG
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let memoizedUrl: Promise<string> | null = null
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines the correct storage server URL based on the current realm.
|
|
31
|
+
*
|
|
32
|
+
* - If `isPreview` is true, uses the realm's baseUrl (localhost)
|
|
33
|
+
* - If the realm's baseUrl contains `.zone`, uses storage.decentraland.zone
|
|
34
|
+
* - Otherwise, uses storage.decentraland.org (production)
|
|
35
|
+
*
|
|
36
|
+
* The realm never changes mid-session, so the result is memoized; a failed
|
|
37
|
+
* resolution is not memoized so transient getRealm errors can be retried.
|
|
38
|
+
*
|
|
39
|
+
* @returns The storage server base URL
|
|
40
|
+
*/
|
|
41
|
+
export function getStorageServerUrl(): Promise<string> {
|
|
42
|
+
if (!memoizedUrl) {
|
|
43
|
+
memoizedUrl = resolveStorageServerUrl()
|
|
44
|
+
memoizedUrl.catch(() => {
|
|
45
|
+
memoizedUrl = null
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
return memoizedUrl
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { signedFetch, SignedFetchRequest } from '~system/SignedFetch'
|
|
2
|
+
import { isServer } from '../network'
|
|
3
|
+
import { errorMessage } from './storage/error-message'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates that the code is running on a server-side scene.
|
|
7
|
+
* Throws an error if called from a client-side context.
|
|
8
|
+
*
|
|
9
|
+
* @param moduleName - The name of the module for the error message
|
|
10
|
+
* @throws Error if not running on a server-side scene
|
|
11
|
+
*/
|
|
12
|
+
export function assertIsServer(moduleName: string): void {
|
|
13
|
+
if (!isServer()) {
|
|
14
|
+
throw new Error(`${moduleName} is only available on server-side scenes`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Result type for operations that can fail.
|
|
20
|
+
* Returns a tuple of [error, null] on failure or [null, data] on success.
|
|
21
|
+
*/
|
|
22
|
+
export type Result<T, E = string> = [E, null] | [null, T]
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extended result type that includes HTTP status code information.
|
|
26
|
+
*/
|
|
27
|
+
export type FetchResult<T> = [string, null, number?] | [null, T, number]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wraps a promise to catch errors and return a Result tuple.
|
|
31
|
+
* This allows for cleaner error handling without try-catch blocks.
|
|
32
|
+
*
|
|
33
|
+
* @param promise - The promise to wrap
|
|
34
|
+
* @returns A tuple of [error, null] on failure or [null, data] on success
|
|
35
|
+
*/
|
|
36
|
+
export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
|
|
37
|
+
try {
|
|
38
|
+
const data = await promise
|
|
39
|
+
return [null, data]
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return [error as E, null]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wraps signedFetch with automatic error handling and JSON parsing. Never rejects.
|
|
47
|
+
* Returns a FetchResult tuple with parsed JSON data or error message and status code.
|
|
48
|
+
*
|
|
49
|
+
* @param signedFetchBody - The signedFetch request configuration
|
|
50
|
+
* @returns A tuple of [error, null, statusCode?] on failure or [null, data, statusCode] on success
|
|
51
|
+
*/
|
|
52
|
+
export async function wrapSignedFetch<T = unknown>(signedFetchBody: SignedFetchRequest): Promise<FetchResult<T>> {
|
|
53
|
+
let response: Awaited<ReturnType<typeof signedFetch>> | undefined
|
|
54
|
+
try {
|
|
55
|
+
response = await signedFetch(signedFetchBody)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Error in ${signedFetchBody.url} endpoint`, { error })
|
|
58
|
+
return [errorMessage(error, 'signedFetch failed'), null, undefined]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!response) {
|
|
62
|
+
console.error(`Error in ${signedFetchBody.url} endpoint: no response`)
|
|
63
|
+
return ['signedFetch returned no response', null, undefined]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorMessage = `${response.status} ${response.statusText}`
|
|
68
|
+
console.error(`Error in ${signedFetchBody.url} endpoint`, { response })
|
|
69
|
+
return [errorMessage, null, response.status]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let body: T
|
|
73
|
+
try {
|
|
74
|
+
// JSON.parse throws synchronously, so it can't be wrapped with tryCatch (the
|
|
75
|
+
// throw would happen while evaluating the argument, rejecting this promise).
|
|
76
|
+
body = JSON.parse(response.body || '{}')
|
|
77
|
+
} catch {
|
|
78
|
+
console.error(`Failed to parse response from ${signedFetchBody.url}`)
|
|
79
|
+
return ['Failed to parse response', null, response.status]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [null, (body ?? {}) as T, response.status]
|
|
83
|
+
}
|