@dcl/sdk 7.24.6-30098879609.commit-e704246 → 7.25.1-30310486734.commit-5ffe873
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/composite-provider.js +9 -4
- 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 +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 +14 -1
- package/network/message-bus-sync.js +161 -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 +7 -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 +79 -0
- package/server/storage/constants.js +19 -0
- package/server/storage/index.d.ts +45 -0
- package/server/storage/index.js +51 -0
- package/server/storage/player.d.ts +45 -0
- package/server/storage/player.js +198 -0
- package/server/storage/scene.d.ts +40 -0
- package/server/storage/scene.js +188 -0
- package/server/storage/value-cache.d.ts +1 -0
- package/server/storage/value-cache.js +52 -0
- package/server/storage/write-queue.d.ts +1 -0
- package/server/storage/write-queue.js +71 -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 +61 -0
- package/src/atom.ts +98 -0
- package/src/composite-provider.ts +10 -3
- package/src/ethereum-provider/text-encoding.d.ts +4 -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 +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 +174 -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 +12 -0
- package/src/server/storage/constants.ts +111 -0
- package/src/server/storage/index.ts +74 -0
- package/src/server/storage/player.ts +289 -0
- package/src/server/storage/scene.ts +270 -0
- package/src/server/storage/value-cache.ts +92 -0
- package/src/server/storage/write-queue.ts +131 -0
- package/src/server/storage-url.ts +49 -0
- package/src/server/utils.ts +76 -0
- package/src/text-codec.ts +12 -59
- package/text-codec.d.ts +8 -23
- package/text-codec.js +13 -55
- package/internal/utf8.d.ts +0 -15
- package/internal/utf8.js +0 -172
- package/src/internal/utf8.ts +0 -167
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DEFAULT_STORAGE_CONFIG, StorageConfigState } from './constants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A cached fact about a key's server-side state.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface CacheEntry {
|
|
8
|
+
/**
|
|
9
|
+
* Serialized `{ value }` body; absent for negative entries. Parsed per read
|
|
10
|
+
* hit so callers never share object references, and compared verbatim for
|
|
11
|
+
* write dedup (exact equality — no hash-collision risk).
|
|
12
|
+
*/
|
|
13
|
+
body?: string
|
|
14
|
+
/** True when the server confirmed the key does not exist (GET 404 or successful DELETE). */
|
|
15
|
+
absent?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bounded, lazily-expiring cache of key states, backing both write dedup
|
|
20
|
+
* (skip storage writes whose value is already known to be stored) and read
|
|
21
|
+
* caching (serve get() without a network request within the TTL).
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export interface ValueCache {
|
|
25
|
+
/** Returns the entry if present and fresh; lazily evicts expired entries. */
|
|
26
|
+
get(key: string): CacheEntry | undefined
|
|
27
|
+
/** Stores or refreshes a known value (serialized body); overwrites negative entries. */
|
|
28
|
+
set(key: string, entry: { body: string }): void
|
|
29
|
+
/** Stores a confirmed-absent (negative) entry, replacing any value entry. */
|
|
30
|
+
setAbsent(key: string): void
|
|
31
|
+
delete(key: string): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the bounded value cache shared by a storage scope.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function createValueCache(config: StorageConfigState): ValueCache {
|
|
39
|
+
const entries = new Map<string, CacheEntry & { storedAt: number }>()
|
|
40
|
+
|
|
41
|
+
function insert(key: string, entry: CacheEntry): void {
|
|
42
|
+
// Delete + re-insert moves refreshed keys to the end of the Map's
|
|
43
|
+
// insertion order, so eviction below drops the least-recently-written.
|
|
44
|
+
entries.delete(key)
|
|
45
|
+
entries.set(key, { ...entry, storedAt: Date.now() })
|
|
46
|
+
|
|
47
|
+
// Guard against misconfiguration: a negative bound would loop forever on
|
|
48
|
+
// an empty map, and a NaN bound would silently disable eviction.
|
|
49
|
+
const maxEntries = Number.isFinite(config.cacheMaxEntries)
|
|
50
|
+
? Math.max(0, config.cacheMaxEntries)
|
|
51
|
+
: DEFAULT_STORAGE_CONFIG.cacheMaxEntries
|
|
52
|
+
|
|
53
|
+
while (entries.size > maxEntries) {
|
|
54
|
+
entries.delete(entries.keys().next().value!)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
get(key: string): CacheEntry | undefined {
|
|
60
|
+
const entry = entries.get(key)
|
|
61
|
+
if (!entry) return undefined
|
|
62
|
+
|
|
63
|
+
// Guard against misconfiguration, mirroring cacheMaxEntries: a NaN
|
|
64
|
+
// bound would silently disable expiry (NaN comparisons are false).
|
|
65
|
+
// Negative values need no guard — they just expire everything.
|
|
66
|
+
const maxAgeMs = Number.isFinite(config.cacheMaxAgeMs)
|
|
67
|
+
? config.cacheMaxAgeMs
|
|
68
|
+
: DEFAULT_STORAGE_CONFIG.cacheMaxAgeMs
|
|
69
|
+
|
|
70
|
+
// Lazy max-age expiry: storedAt is never refreshed on hits, so the age
|
|
71
|
+
// bounds the time since the last actual network confirmation.
|
|
72
|
+
if (Date.now() - entry.storedAt > maxAgeMs) {
|
|
73
|
+
entries.delete(key)
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return entry
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
set(key: string, entry: { body: string }): void {
|
|
81
|
+
insert(key, entry)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
setAbsent(key: string): void {
|
|
85
|
+
insert(key, { absent: true })
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
delete(key: string): void {
|
|
89
|
+
entries.delete(key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pending write operation. `body` is the serialized PUT payload, or null
|
|
3
|
+
* for a DELETE. Callers coalesced into the op share its promise.
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
interface PendingOp {
|
|
7
|
+
body: string | null
|
|
8
|
+
execute: (body: string | null) => Promise<boolean>
|
|
9
|
+
promise: Promise<boolean>
|
|
10
|
+
resolve: (result: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface KeyState {
|
|
14
|
+
/** The op currently on the network. */
|
|
15
|
+
active: PendingOp
|
|
16
|
+
/** At most one queued op; later writes replace its payload (latest wins). */
|
|
17
|
+
queued?: PendingOp
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serializes writes per key so the service commits them in issue order.
|
|
22
|
+
* Overlapping PUTs from the single scene server would otherwise race: the
|
|
23
|
+
* server keeps whichever request it processes last, while the local cache
|
|
24
|
+
* keeps whichever response arrives last — either can disagree with the last
|
|
25
|
+
* set() issued. With at most one in-flight op per key and a single queued
|
|
26
|
+
* "latest value" slot, the server's final state always matches the last
|
|
27
|
+
* write issued, and N rapid writes collapse into at most 2 network calls.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface WriteQueue {
|
|
31
|
+
/**
|
|
32
|
+
* Body of the latest issued write for the key (the queued op if present,
|
|
33
|
+
* else the in-flight one): a string for a PUT, null for a DELETE,
|
|
34
|
+
* undefined when no write is pending.
|
|
35
|
+
*/
|
|
36
|
+
pending(key: string): string | null | undefined
|
|
37
|
+
/** True while any write for the key is in flight or queued. */
|
|
38
|
+
isPending(key: string): boolean
|
|
39
|
+
/**
|
|
40
|
+
* Issues a write. If one is in flight, the new op is queued — replacing any
|
|
41
|
+
* already-queued op, whose callers then follow this op's outcome (their
|
|
42
|
+
* value was superseded before it could ever be observed). An op identical
|
|
43
|
+
* to the queued one joins it; `joinActive` additionally allows joining an
|
|
44
|
+
* identical in-flight op (only valid for dedup-tolerant callers, since that
|
|
45
|
+
* op was issued before this call).
|
|
46
|
+
*/
|
|
47
|
+
enqueue(
|
|
48
|
+
key: string,
|
|
49
|
+
body: string | null,
|
|
50
|
+
execute: (body: string | null) => Promise<boolean>,
|
|
51
|
+
joinActive: boolean
|
|
52
|
+
): Promise<boolean>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates the per-key write serializer shared by a storage scope.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export function createWriteQueue(): WriteQueue {
|
|
60
|
+
const keys = new Map<string, KeyState>()
|
|
61
|
+
|
|
62
|
+
function makeOp(body: string | null, execute: PendingOp['execute']): PendingOp {
|
|
63
|
+
let resolve!: (result: boolean) => void
|
|
64
|
+
const promise = new Promise<boolean>((r) => (resolve = r))
|
|
65
|
+
return { body, execute, promise, resolve }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function drain(key: string, state: KeyState): Promise<void> {
|
|
69
|
+
for (;;) {
|
|
70
|
+
const op = state.active
|
|
71
|
+
let result = false
|
|
72
|
+
try {
|
|
73
|
+
result = await op.execute(op.body)
|
|
74
|
+
} catch {
|
|
75
|
+
// Executors report failures via their boolean result; a throw is
|
|
76
|
+
// unexpected but must not wedge the queue.
|
|
77
|
+
}
|
|
78
|
+
op.resolve(result)
|
|
79
|
+
|
|
80
|
+
if (state.queued) {
|
|
81
|
+
state.active = state.queued
|
|
82
|
+
state.queued = undefined
|
|
83
|
+
} else {
|
|
84
|
+
keys.delete(key)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
pending(key: string): string | null | undefined {
|
|
92
|
+
const state = keys.get(key)
|
|
93
|
+
if (!state) return undefined
|
|
94
|
+
return (state.queued ?? state.active).body
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
isPending(key: string): boolean {
|
|
98
|
+
return keys.has(key)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
enqueue(key: string, body: string | null, execute: PendingOp['execute'], joinActive: boolean): Promise<boolean> {
|
|
102
|
+
const state = keys.get(key)
|
|
103
|
+
|
|
104
|
+
if (!state) {
|
|
105
|
+
const op = makeOp(body, execute)
|
|
106
|
+
const newState: KeyState = { active: op }
|
|
107
|
+
keys.set(key, newState)
|
|
108
|
+
void drain(key, newState)
|
|
109
|
+
return op.promise
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (state.queued) {
|
|
113
|
+
// A queued op has not started, so it is issued "after" this caller
|
|
114
|
+
// either way: join it when identical, supersede it otherwise.
|
|
115
|
+
if (state.queued.body !== body) {
|
|
116
|
+
state.queued.body = body
|
|
117
|
+
state.queued.execute = execute
|
|
118
|
+
}
|
|
119
|
+
return state.queued.promise
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (joinActive && state.active.body === body) {
|
|
123
|
+
return state.active.promise
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const op = makeOp(body, execute)
|
|
127
|
+
state.queued = op
|
|
128
|
+
return op.promise
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -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,76 @@
|
|
|
1
|
+
import { signedFetch, SignedFetchRequest } from '~system/SignedFetch'
|
|
2
|
+
import { isServer } from '../network'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates that the code is running on a server-side scene.
|
|
6
|
+
* Throws an error if called from a client-side context.
|
|
7
|
+
*
|
|
8
|
+
* @param moduleName - The name of the module for the error message
|
|
9
|
+
* @throws Error if not running on a server-side scene
|
|
10
|
+
*/
|
|
11
|
+
export function assertIsServer(moduleName: string): void {
|
|
12
|
+
if (!isServer()) {
|
|
13
|
+
throw new Error(`${moduleName} is only available on server-side scenes`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result type for operations that can fail.
|
|
19
|
+
* Returns a tuple of [error, null] on failure or [null, data] on success.
|
|
20
|
+
*/
|
|
21
|
+
export type Result<T, E = string> = [E, null] | [null, T]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extended result type that includes HTTP status code information.
|
|
25
|
+
*/
|
|
26
|
+
export type FetchResult<T> = [string, null, number?] | [null, T, number]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a promise to catch errors and return a Result tuple.
|
|
30
|
+
* This allows for cleaner error handling without try-catch blocks.
|
|
31
|
+
*
|
|
32
|
+
* @param promise - The promise to wrap
|
|
33
|
+
* @returns A tuple of [error, null] on failure or [null, data] on success
|
|
34
|
+
*/
|
|
35
|
+
export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
|
|
36
|
+
try {
|
|
37
|
+
const data = await promise
|
|
38
|
+
return [null, data]
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return [error as E, null]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wraps signedFetch with automatic error handling and JSON parsing.
|
|
46
|
+
* Returns a FetchResult tuple with parsed JSON data or error message and status code.
|
|
47
|
+
*
|
|
48
|
+
* @param signedFetchBody - The signedFetch request configuration
|
|
49
|
+
* @returns A tuple of [error, null, statusCode?] on failure or [null, data, statusCode] on success
|
|
50
|
+
*/
|
|
51
|
+
export async function wrapSignedFetch<T = unknown>(signedFetchBody: SignedFetchRequest): Promise<FetchResult<T>> {
|
|
52
|
+
const [error, response] = await tryCatch(signedFetch(signedFetchBody))
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
console.error(`Error in ${signedFetchBody.url} endpoint`, { error })
|
|
56
|
+
return [error.message, null, undefined]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorMessage = `${response.status} ${response.statusText}`
|
|
61
|
+
console.error(`Error in ${signedFetchBody.url} endpoint`, { response })
|
|
62
|
+
return [errorMessage, null, response.status]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let body: T
|
|
66
|
+
try {
|
|
67
|
+
// JSON.parse throws synchronously, so it can't be wrapped with tryCatch (the
|
|
68
|
+
// throw would happen while evaluating the argument, rejecting this promise).
|
|
69
|
+
body = JSON.parse(response.body || '{}')
|
|
70
|
+
} catch {
|
|
71
|
+
console.error(`Failed to parse response from ${signedFetchBody.url}`)
|
|
72
|
+
return ['Failed to parse response', null, response.status]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [null, (body ?? {}) as T, response.status]
|
|
76
|
+
}
|
package/src/text-codec.ts
CHANGED
|
@@ -1,70 +1,23 @@
|
|
|
1
|
+
import TextEncodingPolyfill from 'text-encoding'
|
|
1
2
|
import { setGlobalPolyfill } from '@dcl/ecs'
|
|
2
|
-
import { decodeUtf8, encodeUtf8, encodeUtf8Into } from './internal/utf8'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Install the `TextEncoder` / `TextDecoder` polyfill on `globalThis`.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Labels the WHATWG Encoding Standard maps to utf-8.
|
|
12
|
-
const UTF8_LABELS = ['unicode-1-1-utf-8', 'unicode11utf8', 'unicode20utf8', 'utf-8', 'utf8', 'x-unicode20utf8']
|
|
13
|
-
|
|
14
|
-
function toUint8Array(input?: ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
15
|
-
if (input === undefined) return new Uint8Array(0)
|
|
16
|
-
if (input instanceof Uint8Array) return input
|
|
17
|
-
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
|
|
18
|
-
return new Uint8Array(input)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class TextEncoder {
|
|
22
|
-
readonly encoding = 'utf-8'
|
|
23
|
-
|
|
24
|
-
encode(input: string = ''): Uint8Array {
|
|
25
|
-
return encodeUtf8(String(input))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
encodeInto(source: string, destination: Uint8Array): { read: number; written: number } {
|
|
29
|
-
return encodeUtf8Into(String(source), destination)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class TextDecoder {
|
|
34
|
-
readonly encoding = 'utf-8'
|
|
35
|
-
readonly fatal: boolean
|
|
36
|
-
readonly ignoreBOM: boolean
|
|
37
|
-
|
|
38
|
-
constructor(label: string = 'utf-8', options: { fatal?: boolean; ignoreBOM?: boolean } = {}) {
|
|
39
|
-
// the spec strips ASCII whitespace only, so String.prototype.trim is too wide
|
|
40
|
-
const normalized = String(label)
|
|
41
|
-
.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '')
|
|
42
|
-
.toLowerCase()
|
|
43
|
-
if (!UTF8_LABELS.includes(normalized)) {
|
|
44
|
-
throw new RangeError(`TextDecoder: only utf-8 is supported, got "${label}"`)
|
|
45
|
-
}
|
|
46
|
-
this.fatal = options.fatal === true
|
|
47
|
-
this.ignoreBOM = options.ignoreBOM === true
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
decode(input?: ArrayBuffer | ArrayBufferView, options: { stream?: boolean } = {}): string {
|
|
51
|
-
if (options.stream) throw new TypeError('TextDecoder: streaming is not supported')
|
|
52
|
-
return decodeUtf8(toUint8Array(input), { fatal: this.fatal, ignoreBOM: this.ignoreBOM })
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Install `TextEncoder` / `TextDecoder` on `globalThis` where the runtime does
|
|
58
|
-
* not provide them (native implementations always win over these polyfills).
|
|
7
|
+
* The QuickJS scene runtime ships no native `TextEncoder` / `TextDecoder`, yet
|
|
8
|
+
* `compositeProvider.loadComposite` decodes `.composite` JSON file bytes via
|
|
9
|
+
* `TextDecoder`. Callers that load composites at runtime (e.g. `@dcl/asset-packs`'
|
|
10
|
+
* `SPAWN_ENTITY`) must install this first.
|
|
59
11
|
*
|
|
60
12
|
* Exposed from the lean `@dcl/sdk/text-codec` subpath so consumers can install
|
|
61
|
-
* the polyfill without pulling in the ethereum provider
|
|
13
|
+
* the polyfill without pulling in the ethereum provider, and without bundling
|
|
14
|
+
* `text-encoding` into scenes that never reach this module.
|
|
62
15
|
*/
|
|
63
16
|
// NOTE: this function mutates globalThis — it is NOT pure. Do not annotate
|
|
64
17
|
// with /* @__PURE__ */; minifiers would treat the call as dead code and drop
|
|
65
|
-
// the polyfill installation, breaking
|
|
66
|
-
// globalThis.TextDecoder.
|
|
18
|
+
// the polyfill installation, breaking runtime callers like
|
|
19
|
+
// compositeProvider.loadComposite that rely on globalThis.TextDecoder.
|
|
67
20
|
export function polyfillTextEncoder() {
|
|
68
|
-
setGlobalPolyfill('TextEncoder', TextEncoder)
|
|
69
|
-
setGlobalPolyfill('TextDecoder', TextDecoder)
|
|
21
|
+
setGlobalPolyfill('TextEncoder', TextEncodingPolyfill.TextEncoder)
|
|
22
|
+
setGlobalPolyfill('TextDecoder', TextEncodingPolyfill.TextDecoder)
|
|
70
23
|
}
|
package/text-codec.d.ts
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
export declare class TextEncoder {
|
|
2
|
-
readonly encoding = "utf-8";
|
|
3
|
-
encode(input?: string): Uint8Array;
|
|
4
|
-
encodeInto(source: string, destination: Uint8Array): {
|
|
5
|
-
read: number;
|
|
6
|
-
written: number;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
export declare class TextDecoder {
|
|
10
|
-
readonly encoding = "utf-8";
|
|
11
|
-
readonly fatal: boolean;
|
|
12
|
-
readonly ignoreBOM: boolean;
|
|
13
|
-
constructor(label?: string, options?: {
|
|
14
|
-
fatal?: boolean;
|
|
15
|
-
ignoreBOM?: boolean;
|
|
16
|
-
});
|
|
17
|
-
decode(input?: ArrayBuffer | ArrayBufferView, options?: {
|
|
18
|
-
stream?: boolean;
|
|
19
|
-
}): string;
|
|
20
|
-
}
|
|
21
1
|
/**
|
|
22
|
-
* Install `TextEncoder` / `TextDecoder` on `globalThis
|
|
23
|
-
*
|
|
2
|
+
* Install the `TextEncoder` / `TextDecoder` polyfill on `globalThis`.
|
|
3
|
+
*
|
|
4
|
+
* The QuickJS scene runtime ships no native `TextEncoder` / `TextDecoder`, yet
|
|
5
|
+
* `compositeProvider.loadComposite` decodes `.composite` JSON file bytes via
|
|
6
|
+
* `TextDecoder`. Callers that load composites at runtime (e.g. `@dcl/asset-packs`'
|
|
7
|
+
* `SPAWN_ENTITY`) must install this first.
|
|
24
8
|
*
|
|
25
9
|
* Exposed from the lean `@dcl/sdk/text-codec` subpath so consumers can install
|
|
26
|
-
* the polyfill without pulling in the ethereum provider
|
|
10
|
+
* the polyfill without pulling in the ethereum provider, and without bundling
|
|
11
|
+
* `text-encoding` into scenes that never reach this module.
|
|
27
12
|
*/
|
|
28
13
|
export declare function polyfillTextEncoder(): void;
|
package/text-codec.js
CHANGED
|
@@ -1,65 +1,23 @@
|
|
|
1
|
+
import TextEncodingPolyfill from 'text-encoding';
|
|
1
2
|
import { setGlobalPolyfill } from '@dcl/ecs';
|
|
2
|
-
import { decodeUtf8, encodeUtf8, encodeUtf8Into } from './internal/utf8';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Install the `TextEncoder` / `TextDecoder` polyfill on `globalThis`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const UTF8_LABELS = ['unicode-1-1-utf-8', 'unicode11utf8', 'unicode20utf8', 'utf-8', 'utf8', 'x-unicode20utf8'];
|
|
11
|
-
function toUint8Array(input) {
|
|
12
|
-
if (input === undefined)
|
|
13
|
-
return new Uint8Array(0);
|
|
14
|
-
if (input instanceof Uint8Array)
|
|
15
|
-
return input;
|
|
16
|
-
if (ArrayBuffer.isView(input))
|
|
17
|
-
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
18
|
-
return new Uint8Array(input);
|
|
19
|
-
}
|
|
20
|
-
export class TextEncoder {
|
|
21
|
-
constructor() {
|
|
22
|
-
this.encoding = 'utf-8';
|
|
23
|
-
}
|
|
24
|
-
encode(input = '') {
|
|
25
|
-
return encodeUtf8(String(input));
|
|
26
|
-
}
|
|
27
|
-
encodeInto(source, destination) {
|
|
28
|
-
return encodeUtf8Into(String(source), destination);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export class TextDecoder {
|
|
32
|
-
constructor(label = 'utf-8', options = {}) {
|
|
33
|
-
this.encoding = 'utf-8';
|
|
34
|
-
// the spec strips ASCII whitespace only, so String.prototype.trim is too wide
|
|
35
|
-
const normalized = String(label)
|
|
36
|
-
.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '')
|
|
37
|
-
.toLowerCase();
|
|
38
|
-
if (!UTF8_LABELS.includes(normalized)) {
|
|
39
|
-
throw new RangeError(`TextDecoder: only utf-8 is supported, got "${label}"`);
|
|
40
|
-
}
|
|
41
|
-
this.fatal = options.fatal === true;
|
|
42
|
-
this.ignoreBOM = options.ignoreBOM === true;
|
|
43
|
-
}
|
|
44
|
-
decode(input, options = {}) {
|
|
45
|
-
if (options.stream)
|
|
46
|
-
throw new TypeError('TextDecoder: streaming is not supported');
|
|
47
|
-
return decodeUtf8(toUint8Array(input), { fatal: this.fatal, ignoreBOM: this.ignoreBOM });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Install `TextEncoder` / `TextDecoder` on `globalThis` where the runtime does
|
|
52
|
-
* not provide them (native implementations always win over these polyfills).
|
|
6
|
+
* The QuickJS scene runtime ships no native `TextEncoder` / `TextDecoder`, yet
|
|
7
|
+
* `compositeProvider.loadComposite` decodes `.composite` JSON file bytes via
|
|
8
|
+
* `TextDecoder`. Callers that load composites at runtime (e.g. `@dcl/asset-packs`'
|
|
9
|
+
* `SPAWN_ENTITY`) must install this first.
|
|
53
10
|
*
|
|
54
11
|
* Exposed from the lean `@dcl/sdk/text-codec` subpath so consumers can install
|
|
55
|
-
* the polyfill without pulling in the ethereum provider
|
|
12
|
+
* the polyfill without pulling in the ethereum provider, and without bundling
|
|
13
|
+
* `text-encoding` into scenes that never reach this module.
|
|
56
14
|
*/
|
|
57
15
|
// NOTE: this function mutates globalThis — it is NOT pure. Do not annotate
|
|
58
16
|
// with /* @__PURE__ */; minifiers would treat the call as dead code and drop
|
|
59
|
-
// the polyfill installation, breaking
|
|
60
|
-
// globalThis.TextDecoder.
|
|
17
|
+
// the polyfill installation, breaking runtime callers like
|
|
18
|
+
// compositeProvider.loadComposite that rely on globalThis.TextDecoder.
|
|
61
19
|
export function polyfillTextEncoder() {
|
|
62
|
-
setGlobalPolyfill('TextEncoder', TextEncoder);
|
|
63
|
-
setGlobalPolyfill('TextDecoder', TextDecoder);
|
|
20
|
+
setGlobalPolyfill('TextEncoder', TextEncodingPolyfill.TextEncoder);
|
|
21
|
+
setGlobalPolyfill('TextDecoder', TextEncodingPolyfill.TextDecoder);
|
|
64
22
|
}
|
|
65
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGV4dC1jb2RlYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInNyYy90ZXh0LWNvZGVjLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sb0JBQW9CLE1BQU0sZUFBZSxDQUFBO0FBQ2hELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLFVBQVUsQ0FBQTtBQUU1Qzs7Ozs7Ozs7Ozs7R0FXRztBQUNILDJFQUEyRTtBQUMzRSw2RUFBNkU7QUFDN0UsMkRBQTJEO0FBQzNELHVFQUF1RTtBQUN2RSxNQUFNLFVBQVUsbUJBQW1CO0lBQ2pDLGlCQUFpQixDQUFDLGFBQWEsRUFBRSxvQkFBb0IsQ0FBQyxXQUFXLENBQUMsQ0FBQTtJQUNsRSxpQkFBaUIsQ0FBQyxhQUFhLEVBQUUsb0JBQW9CLENBQUMsV0FBVyxDQUFDLENBQUE7QUFDcEUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBUZXh0RW5jb2RpbmdQb2x5ZmlsbCBmcm9tICd0ZXh0LWVuY29kaW5nJ1xuaW1wb3J0IHsgc2V0R2xvYmFsUG9seWZpbGwgfSBmcm9tICdAZGNsL2VjcydcblxuLyoqXG4gKiBJbnN0YWxsIHRoZSBgVGV4dEVuY29kZXJgIC8gYFRleHREZWNvZGVyYCBwb2x5ZmlsbCBvbiBgZ2xvYmFsVGhpc2AuXG4gKlxuICogVGhlIFF1aWNrSlMgc2NlbmUgcnVudGltZSBzaGlwcyBubyBuYXRpdmUgYFRleHRFbmNvZGVyYCAvIGBUZXh0RGVjb2RlcmAsIHlldFxuICogYGNvbXBvc2l0ZVByb3ZpZGVyLmxvYWRDb21wb3NpdGVgIGRlY29kZXMgYC5jb21wb3NpdGVgIEpTT04gZmlsZSBieXRlcyB2aWFcbiAqIGBUZXh0RGVjb2RlcmAuIENhbGxlcnMgdGhhdCBsb2FkIGNvbXBvc2l0ZXMgYXQgcnVudGltZSAoZS5nLiBgQGRjbC9hc3NldC1wYWNrc2AnXG4gKiBgU1BBV05fRU5USVRZYCkgbXVzdCBpbnN0YWxsIHRoaXMgZmlyc3QuXG4gKlxuICogRXhwb3NlZCBmcm9tIHRoZSBsZWFuIGBAZGNsL3Nkay90ZXh0LWNvZGVjYCBzdWJwYXRoIHNvIGNvbnN1bWVycyBjYW4gaW5zdGFsbFxuICogdGhlIHBvbHlmaWxsIHdpdGhvdXQgcHVsbGluZyBpbiB0aGUgZXRoZXJldW0gcHJvdmlkZXIsIGFuZCB3aXRob3V0IGJ1bmRsaW5nXG4gKiBgdGV4dC1lbmNvZGluZ2AgaW50byBzY2VuZXMgdGhhdCBuZXZlciByZWFjaCB0aGlzIG1vZHVsZS5cbiAqL1xuLy8gTk9URTogdGhpcyBmdW5jdGlvbiBtdXRhdGVzIGdsb2JhbFRoaXMg4oCUIGl0IGlzIE5PVCBwdXJlLiBEbyBub3QgYW5ub3RhdGVcbi8vIHdpdGggLyogQF9fUFVSRV9fICovOyBtaW5pZmllcnMgd291bGQgdHJlYXQgdGhlIGNhbGwgYXMgZGVhZCBjb2RlIGFuZCBkcm9wXG4vLyB0aGUgcG9seWZpbGwgaW5zdGFsbGF0aW9uLCBicmVha2luZyBydW50aW1lIGNhbGxlcnMgbGlrZVxuLy8gY29tcG9zaXRlUHJvdmlkZXIubG9hZENvbXBvc2l0ZSB0aGF0IHJlbHkgb24gZ2xvYmFsVGhpcy5UZXh0RGVjb2Rlci5cbmV4cG9ydCBmdW5jdGlvbiBwb2x5ZmlsbFRleHRFbmNvZGVyKCkge1xuICBzZXRHbG9iYWxQb2x5ZmlsbCgnVGV4dEVuY29kZXInLCBUZXh0RW5jb2RpbmdQb2x5ZmlsbC5UZXh0RW5jb2RlcilcbiAgc2V0R2xvYmFsUG9seWZpbGwoJ1RleHREZWNvZGVyJywgVGV4dEVuY29kaW5nUG9seWZpbGwuVGV4dERlY29kZXIpXG59XG4iXX0=
|
package/internal/utf8.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Self-contained UTF-8 codec implementing the WHATWG Encoding Standard
|
|
3
|
-
* algorithms (https://encoding.spec.whatwg.org/), so SDK code never depends
|
|
4
|
-
* on the host providing `TextEncoder` / `TextDecoder` globals.
|
|
5
|
-
*/
|
|
6
|
-
export type DecodeUtf8Options = {
|
|
7
|
-
fatal?: boolean;
|
|
8
|
-
ignoreBOM?: boolean;
|
|
9
|
-
};
|
|
10
|
-
export declare function decodeUtf8(input: Uint8Array, options?: DecodeUtf8Options): string;
|
|
11
|
-
export declare function encodeUtf8(input: string): Uint8Array;
|
|
12
|
-
export declare function encodeUtf8Into(source: string, destination: Uint8Array): {
|
|
13
|
-
read: number;
|
|
14
|
-
written: number;
|
|
15
|
-
};
|