Skip to content

Commit ecf78cb

Browse files
committed
fix(dev): honor processed world reset policy
1 parent bc92424 commit ecf78cb

5 files changed

Lines changed: 140 additions & 5 deletions

File tree

.changeset/quiet-pumas-reseed.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@blurengine/cli": patch
3+
---
4+
5+
Honor `dev.localServer.worldSync.runtimeWorldMode` for processed development worlds, including deterministic replace and backup behavior on `blr dev` startup while preserving script-only hot reloads.

packages/blr/docs/reference/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ Runtime world safety:
166166
- `prompt`: ask before replacing it
167167
- `preserve`: keep it
168168
- runtime backup and replacement only happen before BDS starts
169+
- processed development worlds follow the same startup policy; `replace` rebuilds and reseeds the processed world on each `blr dev` startup without resetting it during script-only hot reloads
169170

170171
Flags:
171172

packages/blr/docs/reference/config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ Notes:
544544
- `replace`: replace the runtime world automatically from the project world before startup
545545
- `backup`: move the existing runtime world into `worlds_backups/` and then replace it before startup
546546
- runtime-world replacement and backup only happen before BDS starts; `blr` does not modify a running server world
547+
- the same `runtimeWorldMode` policy applies when development world processors produce the world that BDS runs; `replace` reseeds that processed world on each `blr dev` startup, while script-only hot reloads preserve the running world
547548
- `watch-world` starts after startup reconciliation and captures runtime world state back into the project source
548549
- `watch-allowlist` captures both retained runtime server-state files:
549550
- `allowlist.json`

packages/blr/src/commands/dev.ts

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
import { buildTrackedProjectWorldFingerprint } from "../project-world-state.js";
4343
import { runPrompt } from "../prompt.js";
4444
import { buildProject, runLocalDeploy } from "../runtime.js";
45-
import type { BlurProject } from "../types.js";
45+
import type { BlurProject, WorldSyncRuntimeMode } from "../types.js";
4646
import {
4747
clearLocalServerSession,
4848
clearRuntimeWorldSeedState,
@@ -972,6 +972,37 @@ type RuntimeWorldDecision =
972972
note?: string;
973973
};
974974

975+
type ProcessedRuntimeWorldDecision = Readonly<{
976+
action: "backup-and-replace" | "preserve" | "prompt" | "replace";
977+
}>;
978+
979+
export function resolveProcessedRuntimeWorldDecision(options: {
980+
reconcileRequested: boolean;
981+
runtimeWorldExists: boolean;
982+
runtimeWorldMode: WorldSyncRuntimeMode;
983+
sourceMatches: boolean;
984+
}): ProcessedRuntimeWorldDecision {
985+
if (!options.runtimeWorldExists) {
986+
return { action: "replace" };
987+
}
988+
if (!options.reconcileRequested) {
989+
return { action: "preserve" };
990+
}
991+
switch (options.runtimeWorldMode) {
992+
case "replace":
993+
return { action: "replace" };
994+
case "backup":
995+
return { action: "backup-and-replace" };
996+
case "preserve":
997+
return { action: "preserve" };
998+
case "prompt":
999+
default:
1000+
return options.sourceMatches
1001+
? { action: "preserve" }
1002+
: { action: "prompt" };
1003+
}
1004+
}
1005+
9751006
function buildRuntimeWorldOutOfSyncMessage(
9761007
worldName: string,
9771008
outcome: string,
@@ -2732,12 +2763,63 @@ export async function runDevCommand(options: DevCommandOptions): Promise<void> {
27322763
),
27332764
)
27342765
: false;
2766+
let processedDecision = resolveProcessedRuntimeWorldDecision({
2767+
reconcileRequested: options.rebuildProcessedWorld,
2768+
runtimeWorldExists,
2769+
runtimeWorldMode:
2770+
config.dev.localServer.worldSync.runtimeWorldMode,
2771+
sourceMatches:
2772+
lastSeed?.sourceIdentity ===
2773+
currentProcessedWorldInput.seedIdentity,
2774+
});
2775+
if (processedDecision.action === "prompt") {
2776+
if (!canPromptForDevWorldSync()) {
2777+
processedDecision = { action: "preserve" };
2778+
console.log(
2779+
buildRuntimeWorldOutOfSyncMessage(
2780+
currentProcessedWorldInput.worldName,
2781+
"keeping existing local-server world because this run is non-interactive",
2782+
),
2783+
);
2784+
} else {
2785+
switch (
2786+
await promptForRuntimeWorldAction(
2787+
currentProcessedWorldInput.worldName,
2788+
)
2789+
) {
2790+
case "replace":
2791+
processedDecision = { action: "replace" };
2792+
break;
2793+
case "backup":
2794+
processedDecision = {
2795+
action: "backup-and-replace",
2796+
};
2797+
break;
2798+
case "keep":
2799+
default:
2800+
processedDecision = { action: "preserve" };
2801+
break;
2802+
}
2803+
}
2804+
}
27352805
const replaceProcessedWorld =
2736-
!runtimeWorldExists ||
2737-
lastSeed?.sourceIdentity !==
2738-
currentProcessedWorldInput.seedIdentity;
2806+
processedDecision.action === "replace" ||
2807+
processedDecision.action === "backup-and-replace";
2808+
if (processedDecision.action === "backup-and-replace") {
2809+
if (!runtimeState) {
2810+
throw new Error(
2811+
"Processed runtime-world backup requires a resolved local-server runtime.",
2812+
);
2813+
}
2814+
await localServer.stop({
2815+
suppressExitNotification: true,
2816+
});
2817+
await backupRuntimeWorldForBdsStartup(runtimeState, debug);
2818+
}
27392819
const processedApplyMode: PipelineMode = replaceProcessedWorld
2740-
? "restart"
2820+
? processedDecision.action === "backup-and-replace"
2821+
? "start"
2822+
: "restart"
27412823
: mode;
27422824
await waitForPromiseIfSlow(
27432825
localServer.apply(processedApplyMode, {

packages/blr/test/dev.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
buildRemoteWorldSyncFailureMessage,
1010
createWorldProcessorWatchPlan,
1111
mergePipelineModes,
12+
resolveProcessedRuntimeWorldDecision,
1213
resolveRuntimeWorldDecision,
1314
resolveProjectWatchChangeAction,
1415
resolveLocalServerLinkOptions,
@@ -467,6 +468,51 @@ test("resolveRuntimeWorldDecision refreshes runtime worlds in replace mode even
467468
assert.equal(decision.action, "replace");
468469
});
469470

471+
test("resolveProcessedRuntimeWorldDecision honors replace mode at startup even when the processed seed matches", () => {
472+
assert.deepEqual(
473+
resolveProcessedRuntimeWorldDecision({
474+
reconcileRequested: true,
475+
runtimeWorldExists: true,
476+
runtimeWorldMode: "replace",
477+
sourceMatches: true,
478+
}),
479+
{ action: "replace" },
480+
);
481+
});
482+
483+
test("resolveProcessedRuntimeWorldDecision preserves a running world for script-only reloads", () => {
484+
assert.deepEqual(
485+
resolveProcessedRuntimeWorldDecision({
486+
reconcileRequested: false,
487+
runtimeWorldExists: true,
488+
runtimeWorldMode: "replace",
489+
sourceMatches: true,
490+
}),
491+
{ action: "preserve" },
492+
);
493+
});
494+
495+
test("resolveProcessedRuntimeWorldDecision keeps processed preserve and backup modes distinct", () => {
496+
assert.deepEqual(
497+
resolveProcessedRuntimeWorldDecision({
498+
reconcileRequested: true,
499+
runtimeWorldExists: true,
500+
runtimeWorldMode: "preserve",
501+
sourceMatches: false,
502+
}),
503+
{ action: "preserve" },
504+
);
505+
assert.deepEqual(
506+
resolveProcessedRuntimeWorldDecision({
507+
reconcileRequested: true,
508+
runtimeWorldExists: true,
509+
runtimeWorldMode: "backup",
510+
sourceMatches: true,
511+
}),
512+
{ action: "backup-and-replace" },
513+
);
514+
});
515+
470516
test("buildRemoteWorldSyncFailureMessage replaces raw unknown backend errors with a helpful dev warning", () => {
471517
assert.match(
472518
buildRemoteWorldSyncFailureMessage({

0 commit comments

Comments
 (0)