Skip to content

Commit 94924cd

Browse files
committed
feat(sdk-commands): namespace and durably persist local dev storage per project
Local preview server storage (server-storage.json) had three issues: - World values were not namespaced by scene, so previewing different scenes shared one bucket. World storage is now keyed by scene base coordinates ("x,y"), read from scene.json. - The file lived inside node_modules/@dcl/sdk-commands, so every SDK upgrade wiped all local dev progress. It now lives in the project's .runtime-data/ directory (threaded via baseDir), surviving `npm i @dcl/sdk@newer` and keeping same-base-coord scenes in different projects isolated. - A legacy flat-format world file was discarded on load. It is now migrated once at preview-server startup into the currently previewed scene's bucket, so no local data is lost on the format change. Reconciles with the upstream serialize() write-lock (#1545): every read-modify-write still runs under the queue.
1 parent 3c77d90 commit 94924cd

10 files changed

Lines changed: 343 additions & 96 deletions

File tree

packages/@dcl/sdk-commands/src/commands/start/server/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export async function wireRouter(components: PreviewComponents, workspace: Works
6868
})
6969

7070
setupRealmAndComms(components, router, localSceneParcels)
71-
setupStorageEndpoints(components, router, workspace)
71+
await setupStorageEndpoints(components, router, workspace)
7272
await setupEcs6Endpoints(components, router, workspace)
7373

7474
components.server.setContext(components)

packages/@dcl/sdk-commands/src/commands/start/server/runtime-env.ts

Lines changed: 114 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
11
import path from 'path'
22
import { CliComponents } from '../../../components'
3+
import { getObject } from '../../../logic/coordinates'
34

4-
// Find the sdk-commands package root by resolving its package.json
5-
const SDK_COMMANDS_ROOT = path.dirname(require.resolve('@dcl/sdk-commands/package.json'))
6-
const RUNTIME_DATA_DIR = path.join(SDK_COMMANDS_ROOT, '.runtime-data')
5+
const RUNTIME_DATA_DIRNAME = '.runtime-data'
76
const SERVER_STORAGE_FILE = 'server-storage.json'
87

8+
const storageDirFor = (baseDir: string): string => path.join(baseDir, RUNTIME_DATA_DIRNAME)
9+
const storagePathFor = (baseDir: string): string => path.join(storageDirFor(baseDir), SERVER_STORAGE_FILE)
10+
911
/**
1012
* Structure for all server-side storage data.
11-
* Stored in sdk-commands package directory (hidden from users).
13+
* Stored in the project's `.runtime-data/` directory so local dev progress survives
14+
* SDK version upgrades (which replace `node_modules`).
15+
*
16+
* `world` is namespaced by scene base coordinates (`"x,y"`) so that previewing
17+
* different scenes does not share the same scene-storage bucket.
1218
*/
1319
export interface ServerStorage {
1420
env: Record<string, string>
15-
world: Record<string, unknown>
21+
world: Record<string, Record<string, unknown>>
1622
players: Record<string, Record<string, unknown>>
1723
}
1824

19-
function createDefaultStorage(): ServerStorage {
20-
return {
21-
env: {},
22-
world: {},
23-
players: {}
24-
}
25+
/**
26+
* Normalizes a scene base parcel (e.g. `"60, -9"`) into the canonical `"x,y"`
27+
* key used to namespace world storage, so equivalent spellings map to one bucket.
28+
*/
29+
export function getSceneStorageKey(base: string): string {
30+
const { x, y } = getObject(base)
31+
return `${x},${y}`
2532
}
2633

34+
const isPlainObject = (value: unknown): boolean => typeof value === 'object' && value !== null && !Array.isArray(value)
35+
36+
const createDefaultStorage = (): ServerStorage => ({
37+
env: {},
38+
world: {},
39+
players: {}
40+
})
41+
2742
let writeQueue: Promise<unknown> = Promise.resolve()
2843

2944
/**
@@ -44,11 +59,12 @@ function serialize<T>(task: () => Promise<T>): Promise<T> {
4459
/**
4560
* Ensures the runtime data directory exists.
4661
*/
47-
async function ensureRuntimeDir(components: Pick<CliComponents, 'fs' | 'logger'>): Promise<void> {
62+
async function ensureRuntimeDir(components: Pick<CliComponents, 'fs' | 'logger'>, baseDir: string): Promise<void> {
63+
const dir = storageDirFor(baseDir)
4864
try {
49-
const exists = await components.fs.directoryExists(RUNTIME_DATA_DIR)
65+
const exists = await components.fs.directoryExists(dir)
5066
if (!exists) {
51-
await components.fs.mkdir(RUNTIME_DATA_DIR, { recursive: true })
67+
await components.fs.mkdir(dir, { recursive: true })
5268
}
5369
} catch (error) {
5470
components.logger.error(`Failed to create runtime data directory: ${error}`)
@@ -58,8 +74,11 @@ async function ensureRuntimeDir(components: Pick<CliComponents, 'fs' | 'logger'>
5874
/**
5975
* Loads all server-side storage data from server-storage.json.
6076
*/
61-
export async function loadServerStorage(components: Pick<CliComponents, 'fs' | 'logger'>): Promise<ServerStorage> {
62-
const storagePath = path.join(RUNTIME_DATA_DIR, SERVER_STORAGE_FILE)
77+
export async function loadServerStorage(
78+
components: Pick<CliComponents, 'fs' | 'logger'>,
79+
baseDir: string
80+
): Promise<ServerStorage> {
81+
const storagePath = storagePathFor(baseDir)
6382

6483
try {
6584
const exists = await components.fs.fileExists(storagePath)
@@ -86,10 +105,11 @@ export async function loadServerStorage(components: Pick<CliComponents, 'fs' | '
86105
*/
87106
export async function saveServerStorage(
88107
components: Pick<CliComponents, 'fs' | 'logger'>,
108+
baseDir: string,
89109
data: ServerStorage
90110
): Promise<void> {
91-
await ensureRuntimeDir(components)
92-
const storagePath = path.join(RUNTIME_DATA_DIR, SERVER_STORAGE_FILE)
111+
await ensureRuntimeDir(components, baseDir)
112+
const storagePath = storagePathFor(baseDir)
93113

94114
try {
95115
const tmpPath = `${storagePath}.tmp`
@@ -101,6 +121,30 @@ export async function saveServerStorage(
101121
}
102122
}
103123

124+
/**
125+
* Migrates a legacy flat-format world file (`world: key -> value`, written before
126+
* scene-coordinate namespacing) into the currently-previewed scene's bucket, so no
127+
* local dev data is lost on the format change. No-op once the file is already
128+
* namespaced (`world: "x,y" -> key -> value`).
129+
*/
130+
export async function migrateLegacyWorldStorage(
131+
components: Pick<CliComponents, 'fs' | 'logger'>,
132+
baseDir: string,
133+
sceneKey: string
134+
): Promise<void> {
135+
return serialize(async () => {
136+
const storage = await loadServerStorage(components, baseDir)
137+
const world = storage.world ?? {}
138+
const isLegacyFlat = Object.values(world).some((value) => !isPlainObject(value))
139+
if (!isLegacyFlat) {
140+
return
141+
}
142+
components.logger.debug(`Migrating legacy flat world storage into scene bucket ${sceneKey}`)
143+
storage.world = { [sceneKey]: world }
144+
await saveServerStorage(components, baseDir, storage)
145+
})
146+
}
147+
104148
/**
105149
* Loads environment variables from a .env file in the project directory.
106150
* Returns a Map of key-value pairs.
@@ -123,7 +167,6 @@ export async function loadEnvFile(
123167

124168
for (const line of lines) {
125169
const trimmed = line.trim()
126-
// Skip empty lines and comments
127170
if (!trimmed || trimmed.startsWith('#')) {
128171
continue
129172
}
@@ -133,7 +176,6 @@ export async function loadEnvFile(
133176
const key = trimmed.slice(0, equalIndex).trim()
134177
let value = trimmed.slice(equalIndex + 1).trim()
135178

136-
// Remove surrounding quotes if present
137179
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
138180
value = value.slice(1, -1)
139181
}
@@ -151,23 +193,26 @@ export async function loadEnvFile(
151193
/**
152194
* Gets runtime environment variables.
153195
*/
154-
export async function getEnvStorage(components: Pick<CliComponents, 'fs' | 'logger'>): Promise<Record<string, string>> {
155-
const storage = await loadServerStorage(components)
196+
export async function getEnvStorage(
197+
components: Pick<CliComponents, 'fs' | 'logger'>,
198+
baseDir: string
199+
): Promise<Record<string, string>> {
200+
const storage = await loadServerStorage(components, baseDir)
156201
return storage.env
157202
}
158203

159204
/**
160205
* Gets merged environment variables.
161206
* Runtime values (from server-storage.json) override .env values.
207+
* The storage file and the `.env` file share the project directory.
162208
*/
163209
export async function getMergedEnv(
164210
components: Pick<CliComponents, 'fs' | 'logger'>,
165211
projectDirectory: string
166212
): Promise<Map<string, string>> {
167213
const envFile = await loadEnvFile(components, projectDirectory)
168-
const runtimeEnv = await getEnvStorage(components)
214+
const runtimeEnv = await getEnvStorage(components, projectDirectory)
169215

170-
// Runtime overrides .env
171216
for (const [key, value] of Object.entries(runtimeEnv)) {
172217
envFile.set(key, value)
173218
}
@@ -180,83 +225,99 @@ export async function getMergedEnv(
180225
*/
181226
export async function setEnvValue(
182227
components: Pick<CliComponents, 'fs' | 'logger'>,
228+
baseDir: string,
183229
key: string,
184230
value: string
185231
): Promise<void> {
186232
return serialize(async () => {
187-
const storage = await loadServerStorage(components)
233+
const storage = await loadServerStorage(components, baseDir)
188234
storage.env[key] = value
189-
await saveServerStorage(components, storage)
235+
await saveServerStorage(components, baseDir, storage)
190236
})
191237
}
192238

193239
/**
194240
* Deletes a runtime environment variable.
195241
* Returns true if key existed and was deleted, false otherwise.
196242
*/
197-
export async function deleteEnvValue(components: Pick<CliComponents, 'fs' | 'logger'>, key: string): Promise<boolean> {
243+
export async function deleteEnvValue(
244+
components: Pick<CliComponents, 'fs' | 'logger'>,
245+
baseDir: string,
246+
key: string
247+
): Promise<boolean> {
198248
return serialize(async () => {
199-
const storage = await loadServerStorage(components)
249+
const storage = await loadServerStorage(components, baseDir)
200250
if (!(key in storage.env)) {
201251
return false
202252
}
203253
delete storage.env[key]
204-
await saveServerStorage(components, storage)
254+
await saveServerStorage(components, baseDir, storage)
205255
return true
206256
})
207257
}
208258

209259
/**
210-
* Gets all world storage data.
260+
* Gets all world storage data for a scene, keyed by its base-coordinate bucket.
211261
*/
212262
export async function getWorldStorage(
213-
components: Pick<CliComponents, 'fs' | 'logger'>
263+
components: Pick<CliComponents, 'fs' | 'logger'>,
264+
baseDir: string,
265+
sceneKey: string
214266
): Promise<Record<string, unknown>> {
215-
const storage = await loadServerStorage(components)
216-
return storage.world
267+
const storage = await loadServerStorage(components, baseDir)
268+
return storage.world[sceneKey] ?? {}
217269
}
218270

219271
/**
220-
* Gets a value from world storage.
272+
* Gets a value from a scene's world storage.
221273
*/
222274
export async function getWorldValue(
223275
components: Pick<CliComponents, 'fs' | 'logger'>,
276+
baseDir: string,
277+
sceneKey: string,
224278
key: string
225279
): Promise<unknown | undefined> {
226-
const storage = await loadServerStorage(components)
227-
return storage.world[key]
280+
const storage = await loadServerStorage(components, baseDir)
281+
return storage.world[sceneKey]?.[key]
228282
}
229283

230284
/**
231-
* Sets a value in world storage.
285+
* Sets a value in a scene's world storage.
232286
*/
233287
export async function setWorldValue(
234288
components: Pick<CliComponents, 'fs' | 'logger'>,
289+
baseDir: string,
290+
sceneKey: string,
235291
key: string,
236292
value: unknown
237293
): Promise<void> {
238294
return serialize(async () => {
239-
const storage = await loadServerStorage(components)
240-
storage.world[key] = value
241-
await saveServerStorage(components, storage)
295+
const storage = await loadServerStorage(components, baseDir)
296+
if (!storage.world[sceneKey]) {
297+
storage.world[sceneKey] = {}
298+
}
299+
storage.world[sceneKey][key] = value
300+
await saveServerStorage(components, baseDir, storage)
242301
})
243302
}
244303

245304
/**
246-
* Deletes a value from world storage.
305+
* Deletes a value from a scene's world storage.
247306
* Returns true if key existed and was deleted, false otherwise.
248307
*/
249308
export async function deleteWorldValue(
250309
components: Pick<CliComponents, 'fs' | 'logger'>,
310+
baseDir: string,
311+
sceneKey: string,
251312
key: string
252313
): Promise<boolean> {
253314
return serialize(async () => {
254-
const storage = await loadServerStorage(components)
255-
if (!(key in storage.world)) {
315+
const storage = await loadServerStorage(components, baseDir)
316+
if (!storage.world[sceneKey] || !(key in storage.world[sceneKey])) {
256317
return false
257318
}
258-
delete storage.world[key]
259-
await saveServerStorage(components, storage)
319+
delete storage.world[sceneKey][key]
320+
await saveServerStorage(components, baseDir, storage)
260321
return true
261322
})
262323
}
@@ -266,10 +327,11 @@ export async function deleteWorldValue(
266327
*/
267328
export async function getPlayerValue(
268329
components: Pick<CliComponents, 'fs' | 'logger'>,
330+
baseDir: string,
269331
address: string,
270332
key: string
271333
): Promise<unknown | undefined> {
272-
const storage = await loadServerStorage(components)
334+
const storage = await loadServerStorage(components, baseDir)
273335
return storage.players[address]?.[key]
274336
}
275337

@@ -278,17 +340,18 @@ export async function getPlayerValue(
278340
*/
279341
export async function setPlayerValue(
280342
components: Pick<CliComponents, 'fs' | 'logger'>,
343+
baseDir: string,
281344
address: string,
282345
key: string,
283346
value: unknown
284347
): Promise<void> {
285348
return serialize(async () => {
286-
const storage = await loadServerStorage(components)
349+
const storage = await loadServerStorage(components, baseDir)
287350
if (!storage.players[address]) {
288351
storage.players[address] = {}
289352
}
290353
storage.players[address][key] = value
291-
await saveServerStorage(components, storage)
354+
await saveServerStorage(components, baseDir, storage)
292355
})
293356
}
294357

@@ -298,16 +361,17 @@ export async function setPlayerValue(
298361
*/
299362
export async function deletePlayerValue(
300363
components: Pick<CliComponents, 'fs' | 'logger'>,
364+
baseDir: string,
301365
address: string,
302366
key: string
303367
): Promise<boolean> {
304368
return serialize(async () => {
305-
const storage = await loadServerStorage(components)
369+
const storage = await loadServerStorage(components, baseDir)
306370
if (!storage.players[address] || !(key in storage.players[address])) {
307371
return false
308372
}
309373
delete storage.players[address][key]
310-
await saveServerStorage(components, storage)
374+
await saveServerStorage(components, baseDir, storage)
311375
return true
312376
})
313377
}

0 commit comments

Comments
 (0)