fix: fsEntry and tscofnig (#2923)

This commit is contained in:
Daniel Salazar
2026-05-05 16:18:09 -07:00
committed by GitHub
parent 4252b7f76f
commit f9ac017faf
5 changed files with 147 additions and 18 deletions
+1 -1
View File
@@ -53,7 +53,7 @@
"build:workerLib": "cd src/puter-js && npm run build && cd ../worker && npm run build",
"check-translations": "node tools/check-translations.js",
"prepare": "husky",
"build:ts": "tsc -p tsconfig.json && node ./tools/write-dist-package-json.mjs",
"build:ts": "tsc -p tsconfig.build.json && node ./tools/write-dist-package-json.mjs",
"postinstall": "node ./tools/extensionSetup.mjs"
},
"workspaces": [
+1 -1
View File
@@ -72,7 +72,7 @@ The Puter Team
Please update <a href="https://apps.puter.com/app/{{app_name}}">{{app_title}}</a>.
</p>
<p><strong>Requested updates:</strong></p>
<blockquote>{{message}}</blockquote>
<blockquote>{{nl2br message}}</blockquote>
<p>Best,<br />
The Puter Team
</p>
+133 -1
View File
@@ -1836,8 +1836,14 @@ export class FSEntryStore extends PuterStore {
);
}
// Use INSERT IGNORE so a row that lost the (parent_id,
// name) race against another writer doesn't take down
// the whole chunk. Skipped rows are recovered below by
// detecting uuids missing from the post-INSERT SELECT
// and running the same overwrite-or-409 logic the
// upfront existence check uses.
await this.clients.db.write(
`INSERT INTO fsentries (
`${this.#insertIgnoreIntoFsentriesSql()} (
uuid,
bucket,
bucket_region,
@@ -1908,6 +1914,132 @@ export class FSEntryStore extends PuterStore {
}
}
// Recovery for INSERT IGNORE skips. An insertCandidate whose
// uuid is missing from the post-INSERT SELECT lost the
// (parent_id, name) race against a concurrent writer. Re-check
// primary-aware and either UPDATE (overwrite=true) or surface
// a clear 409. Other rows in the batch already persisted.
const racedCandidates = insertCandidates.filter(
(entry) => !insertedEntriesByUuid.has(entry.input.uuid),
);
const insertConflictErrors: Error[] = [];
if (racedCandidates.length > 0) {
const racedExistingByPath =
await this.#readEntriesByPathsForUser(
userId,
racedCandidates.map((entry) => entry.targetPath),
{ useTryHardRead: true, skipCache: true },
);
for (const entry of racedCandidates) {
const parentEntry = parentByPath.get(entry.parentPath);
if (!parentEntry) {
throw new Error(
`Failed to resolve parent directory for ${entry.targetPath}`,
);
}
const existing = racedExistingByPath.get(entry.targetPath);
if (!existing) {
insertConflictErrors.push(
new HttpError(
409,
`Entry already exists at ${entry.targetPath}`,
),
);
continue;
}
if (existing.isDir) {
insertConflictErrors.push(
new HttpError(
409,
`Cannot overwrite a directory at ${entry.targetPath}`,
),
);
continue;
}
if (!entry.input.overwrite) {
insertConflictErrors.push(
new HttpError(
409,
`Entry already exists at ${entry.targetPath}`,
),
);
continue;
}
const updatedEntry: FSEntry = {
...existing,
bucket: entry.bucket,
bucketRegion: entry.bucketRegion,
parentId: parentEntry.id,
parentUid: parentEntry.uuid,
associatedAppId: entry.input.associatedAppId ?? null,
isPublic:
entry.input.isPublic === undefined
? null
: Boolean(entry.input.isPublic),
thumbnail: entry.input.thumbnail ?? null,
immutable: Boolean(entry.input.immutable),
name: entry.fileName,
path: entry.targetPath,
metadata: entry.metadataJson,
modified: now,
accessed: now,
size: entry.size,
};
await this.clients.db.write(
`UPDATE fsentries
SET bucket = ?,
bucket_region = ?,
parent_id = ?,
parent_uid = ?,
associated_app_id = ?,
is_public = ?,
thumbnail = ?,
immutable = ?,
name = ?,
path = ?,
metadata = ?,
modified = ?,
accessed = ?,
size = ?
WHERE id = ?`,
[
entry.bucket,
entry.bucketRegion,
parentEntry.id,
parentEntry.uuid,
entry.input.associatedAppId ?? null,
entry.input.isPublic === undefined
? null
: entry.input.isPublic
? 1
: 0,
entry.input.thumbnail ?? null,
entry.input.immutable ? 1 : 0,
entry.fileName,
entry.targetPath,
entry.metadataJson,
now,
now,
entry.size,
existing.id,
],
);
await this.#invalidateEntryCache(existing);
await this.#writeEntryToCache(updatedEntry);
updatedResultsByIndex.set(entry.index, updatedEntry);
}
}
if (insertConflictErrors.length > 0) {
// Other rows in this user's batch already persisted (the
// INSERT IGNORE landed them and overwrite=true racers ran
// UPDATE above). Surface the first non-recoverable conflict
// as a clean 409 instead of letting ER_DUP_ENTRY escape
// through SQLBatcher as a 500.
throw insertConflictErrors[0]!;
}
for (const entry of userEntries) {
const updatedResult = updatedResultsByIndex.get(entry.index);
if (updatedResult) {
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": false,
"noCheck": true,
"noImplicitAny": true
}
}
+4 -15
View File
@@ -19,7 +19,7 @@
},
"allowJs": true,
"checkJs": false,
"noCheck": true,
"noCheck": false,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
@@ -29,7 +29,7 @@
"removeComments": true,
"noEmit": false,
"noEmitOnError": false,
"noImplicitAny": false
"noImplicitAny": true
},
"include": [
"src/backend/**/*",
@@ -44,17 +44,6 @@
"**/node_modules/**",
"dist/**",
"volatile/**",
"extensions/**/node_modules/**",
"src/backend/test/**",
"src/backend/tools/**",
"src/backend/vitest.config.ts",
"src/backend/vitest.bench.config.ts",
"src/backend/vitest.bench.config.js",
"src/backend/services/worker/template/puter-portable.js",
"src/backend/services/DynamoKVStore/DynamoKVStore.ts",
"src/backend/clients/s3/S3Client.js",
"src/backend/clients/s3/s3ClientProvider.js",
"src/backend/clients/redis/RedisClient.js",
"src/backend/clients/dynamodb/DDBClient.js"
]
"extensions/**/node_modules/**"
]
}