{"record":{"id":"34aa45a92baed62c","repo":"agalwood/Motrix","slug":"plugin-storage-cas-mismatch","errorCode":"plugin.storage.cas_mismatch","errorMessage":"plugin.storage.cas_mismatch: key already exists (expectedVersion=0)","messagePattern":"plugin\\.storage\\.cas_mismatch: key already exists \\(expectedVersion=0\\)","errorType":"exception","errorClass":"StorageError","httpStatus":null,"severity":"warning","filePath":"src/core/plugin/capabilities/storage.ts","lineNumber":240,"sourceCode":"\n    const now = Date.now()\n\n    if (expectedVersion === 0) {\n      // Insert only if absent\n      const row = this.db\n        .prepare<\n          [string, string, string, number, number],\n          { version: number } | undefined\n        >(\n          `INSERT INTO plugin_storage (plugin_id, key, value, size, version, updated_at)\n           VALUES (?, ?, ?, ?, 1, ?)\n           ON CONFLICT(plugin_id, key) DO NOTHING\n           RETURNING version`\n        )\n        .get(pluginId, key, json, size, now)\n\n      if (!row) {\n        throw new StorageError(\n          'plugin.storage.cas_mismatch',\n          'plugin.storage.cas_mismatch: key already exists (expectedVersion=0)'\n        )\n      }\n      return { version: row.version }\n    }\n\n    // Update only when stored version matches expectedVersion\n    const row = this.db\n      .prepare<\n        [string, number, number, string, string, number],\n        { version: number } | undefined\n      >(\n        `UPDATE plugin_storage\n         SET value = ?, size = ?, version = version + 1, updated_at = ?\n         WHERE plugin_id = ? AND key = ? AND version = ?\n         RETURNING version`\n      )","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/storage.ts#L222-L258","documentation":"Thrown by compareAndSet() when expectedVersion === 0 (the 'insert only if absent' intent) but a row for (pluginId, key) already exists. The implementation uses INSERT ... ON CONFLICT DO NOTHING RETURNING version; if it returns no row, the key was present, so the optimistic-insert precondition failed. No version is returned.","triggerScenarios":"Calling host.compareAndSet(pluginId, key, 0, value) when any prior set/compareAndSet already created the row (version >= 1). The intended-create path is the only one that uses expectedVersion=0.","commonSituations":"A plugin uses compareAndSet(id, 0, ...) to initialize a value and races with another activation that already created it; a developer calls set() to seed a default and then compareAndSet(...,0,...) expecting to own the insert; retry logic re-runs an init step that already succeeded.","solutions":["Treat this as a normal CAS conflict, not a crash: read the current version with get() and switch to an update with the real expectedVersion.","If you only need idempotent initialization, use set() (unconditional upsert) instead of compareAndSet(...,0,...).","For create-if-absent semantics with a follow-up, catch the StorageError, inspect .code === 'plugin.storage.cas_mismatch', and fall through to a read-then-update flow.","Guard concurrency at a higher level (a lock/mutex around init) so only one writer attempts the version-0 insert."],"exampleFix":"// before\nawait host.compareAndSet(pluginId, 'cfg', 0, defaults)\n\n// after\ntry {\n  await host.compareAndSet(pluginId, 'cfg', 0, defaults)\n} catch (e) {\n  if (e.code === 'plugin.storage.cas_mismatch') {\n    const { version } = await host.get(pluginId, 'cfg')\n    await host.compareAndSet(pluginId, 'cfg', version, merged)\n  } else throw e\n}","handlingStrategy":"try-catch","validationCode":"const { version } = await host.get(pluginId, key)\nif (version !== 0) { /* key exists; use update path, not insert */ }","typeGuard":null,"tryCatchPattern":"try { await host.compareAndSet(pluginId, key, 0, value) }\ncatch (e) { if (e.code === 'plugin.storage.cas_mismatch') { const { version } = await host.get(pluginId, key); await host.compareAndSet(pluginId, key, version, merged) } else throw e }","preventionTips":["Use compareAndSet(...,0,...) only for create-if-absent; use set() for idempotent init.","Wrap the insert in a try/catch that falls through to read-then-update.","Serialize plugin initialization to avoid racing inserts."],"tags":["storage","cas","concurrency","plugin"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}