Skip to content

Commit e00a266

Browse files
Stamp X-Codeflow-Rev before opening the snapshot fd. An atomic save after fs.open can bump the path rev while the handle still reads the old inode; sampling after open then made retainCliWatchPathsAfterAnalysis treat that SSE event as already in the snapshot.
Co-authored-by: Braedon Saunders <braedonsaunders@users.noreply.github.com>
1 parent a79503e commit e00a266

2 files changed

Lines changed: 13 additions & 2 deletions

File tree

cli/codeflow.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,23 @@ function sendFileError(res, status, message) {
221221
res.end(message);
222222
}
223223

224+
function resolveSnapshotRev(options) {
225+
options = options || {};
226+
return typeof options.snapshotRev === 'function' ? options.snapshotRev() : options.snapshotRev;
227+
}
228+
224229
function pipeSafeFile(res, filePath, contentType, maxBytes, options) {
225230
options = options || {};
231+
// Sample before open so an atomic save cannot bump the path rev onto
232+
// bytes later read from the already-bound (stale) inode.
233+
const snapshotRev = resolveSnapshotRev(options);
226234
return fs.open(filePath, 'r').then(async (fh) => {
227235
try {
228236
const st = await fh.stat();
229237
if (!st.isFile()) {
230238
sendFileError(res, 404, 'Not found');
231239
return;
232240
}
233-
const snapshotRev = typeof options.snapshotRev === 'function' ? options.snapshotRev() : options.snapshotRev;
234241
if (Number.isFinite(maxBytes) && st.size > maxBytes) {
235242
sendFileError(res, 413, 'File too large');
236243
return;

tests/cli-codeflow.test.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,12 @@ test('CLI server serves the same UI and folder files', async (t) => {
133133
assert.match(fileBody, /function/);
134134
assert.equal(file.headers.get('content-length'), String(Buffer.byteLength(fileBody)));
135135
assert.equal(file.headers.get('x-codeflow-rev'), '0');
136+
// An atomic save after open would bump the live rev; the header must
137+
// still reflect the pre-open sample so retainCliWatchPathsAfterAnalysis
138+
// treats that SSE rev as newer than the snapshot.
136139
const cliSource = await readFile(join(repoRoot, 'cli/codeflow.mjs'), 'utf8');
137-
assert.match(cliSource, /function pipeSafeFile[\s\S]*?fs\.open\(filePath, 'r'\)[\s\S]*?readOpenedSnapshot\(fh, st\.size\)/);
140+
assert.match(cliSource, /function pipeSafeFile[\s\S]*?resolveSnapshotRev\(options\)[\s\S]*?fs\.open\(filePath, 'r'\)[\s\S]*?readOpenedSnapshot\(fh, st\.size\)/);
141+
assert.doesNotMatch(cliSource, /function pipeSafeFile[\s\S]*?fs\.open\(filePath, 'r'\)[\s\S]*?resolveSnapshotRev\(/);
138142
assert.match(cliSource, /isMissingFsError\(err\)[\s\S]*?sendFileError\(res, 404/);
139143
assert.match(cliSource, /sendFileError\(res, 500, 'Read failed'\)/);
140144
assert.match(cliSource, /X-Codeflow-Rev/);

0 commit comments

Comments
 (0)