Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/debounce-unchanged-env-restart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@varlock/cloudflare-integration": patch
---

fix(cloudflare): debounce wrangler restarts and skip when env is unchanged

`varlock-wrangler dev` now caches the serialized resolved env graph and compares it
after each debounced watch event. Wrangler only restarts when the resolved env has
actually changed, preventing restart loops caused by spurious `fs.watch()` events
on macOS (which can emit events even when file contents are identical).
52 changes: 52 additions & 0 deletions framework-tests/frameworks/cloudflare/wrangler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,58 @@ describe('Cloudflare Workers varlock-wrangler only', () => {
],
});

wranglerEnv.describeDevScenario('no restart on unchanged env content', {
command: 'varlock-wrangler dev --port 18792',
readyPattern: /Ready on|ready in/i,
readyTimeout: 30_000,
templateFiles: {
'src/index.ts': { path: 'workers/basic-worker.ts', prepend: "import '@varlock/cloudflare-integration/init';\n" },
'wrangler.jsonc': '_base-wrangler/wrangler.jsonc',
'tsconfig.json': '_base-wrangler/tsconfig.json',
},
requests: [
// first request — baseline
{
path: '/',
bodyAssertions: {
shouldContain: ['public_var::public-test-value'],
},
},
// second request — write the same .env.schema content (simulates macOS spurious watch events)
// use fileEditDelay so we wait without expecting a restart
{
path: '/',
fileEdits: {
'.env.schema': [
'# @defaultSensitive=false @defaultRequired=infer',
'# @currentEnv=$APP_ENV',
'# ---',
'',
'# @type=enum(dev, prod)',
'APP_ENV=dev',
'',
'PUBLIC_VAR=public-test-value',
'API_URL=https://api.example.com',
'',
'# @sensitive',
'SECRET_KEY=super-secret-value',
].join('\n'),
},
// wait longer than the 300ms debounce to confirm no restart occurred
fileEditDelay: 1500,
bodyAssertions: {
shouldContain: ['public_var::public-test-value'],
},
},
],
outputAssertions: [
{
description: 'wrangler is not restarted when env content is unchanged',
shouldNotContain: ['env changed, restarting wrangler'],
},
],
});

describe('invalid config', () => {
wranglerEnv.describeScenario('invalid schema causes dev failure', {
command: 'varlock-wrangler dev --port 18791',
Expand Down
23 changes: 21 additions & 2 deletions packages/integrations/cloudflare/src/varlock-wrangler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,21 +412,40 @@ async function handleDev(args: Array<string>) {
});

// watch env source files for changes and restart wrangler with fresh data
const DEBOUNCE_MS = 300;
// force a restart if the last save event was more than this long after the previous restart,
// even when the resolved env is identical — handles repeated saves of an unchanged file
const FORCE_RESTART_IDLE_MS = 5000;
let restartTimeout: ReturnType<typeof setTimeout> | undefined;
let cachedGraphJson = loaded.json;
let lastRestartAt = Date.now();
function scheduleRestart() {
// debounce — multiple files may change at once
// debounce — multiple files may change at once (e.g. editor saves multiple files,
// or macOS fs.watch() emits extra events for unchanged files)
if (restartTimeout) clearTimeout(restartTimeout);
restartTimeout = setTimeout(() => {
try {
const freshLoaded = loadSerializedGraph();
const now = Date.now();
const idleSinceLastRestart = now - lastRestartAt > FORCE_RESTART_IDLE_MS;
// skip restart only when env is unchanged AND a restart happened recently
if (freshLoaded.json === cachedGraphJson && !idleSinceLastRestart) {
restartTimeout = undefined;
return;
}
cachedGraphJson = freshLoaded.json;
loaded = freshLoaded;
lastRestartAt = now;
cachedContent = formatEnvFileContent(freshLoaded);
handle.update(cachedContent);
console.log('[varlock-wrangler] env changed, restarting wrangler...');
wranglerChild?.kill();
// NOTE: restartTimeout stays truthy so the exit handler knows this was a restart-kill
} catch (err) {
restartTimeout = undefined;
console.error('[varlock-wrangler] failed to re-resolve env:', (err as Error).message);
}
}, 300);
}, DEBOUNCE_MS);
}

// set up watchers on env source files
Expand Down
Loading