11import path from 'path'
22import { 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'
76const 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 */
1319export 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+
2742let 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 */
87106export 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 */
163209export 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 */
181226export 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 */
212262export 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 */
222274export 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 */
233287export 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 */
249308export 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 */
267328export 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 */
279341export 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 */
299362export 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