Conversation
Conditional `setItem` / `setItemRaw` writes:
storage.setItem(key, value, { ifNoneMatch: "*" }) // create-only
storage.setItem(key, value, { ifMatch: meta.etag }) // optimistic swap
Drivers expose `etag` via `getMeta` and throw `CASMismatchError` on
precondition failure. Drivers without `flags.cas` throw
`CASUnsupportedError` upfront so silent precondition-skip is impossible.
Implemented in: memory, lru-cache, fs (atomic create via temp+link;
single-process etag mutex), redis (SET NX + WATCH/MULTI/EXEC, content
SHA-1 etag).
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Adds ifMatch/ifNoneMatch CAS to deno-kv (+node), mongodb, s3, cloudflare-r2-binding, netlify-blobs, vercel-blob, db0, http (+server), and overlay. Native primitives where available; emulated with checkCAS + in-process lock where not. - deno-kv: atomic check/set with versionstamp as etag - mongodb: insertOne/updateOne with _etag (SHA-1) field + unique index - s3: If-Match/If-None-Match headers, 412/409 -> mismatch - cloudflare-r2-binding: onlyIf etagMatches/etagDoesNotMatch - netlify-blobs: onlyIfNew/onlyIfMatch (native + emulated for *) - vercel-blob: native ifMatch + head-then-write for other shapes - db0: SELECT-then-write under in-process lock; new etag column - http: forward If-Match/If-None-Match; server maps 412/501 - overlay: delegate to top layer; flags.cas derives from layers[0]
ifMatch, ifNoneMatch
ifMatch, ifNoneMatchifMatch, ifNoneMatch
- azure-cosmos: items.create + replace with IfMatch accessCondition - azure-storage-blob: native conditions.ifMatch / ifNoneMatch - azure-storage-table: createEntity + updateEntity with etag - azure-app-configuration: addConfigurationSetting + setConfigurationSetting onlyIfUnchanged - planetscale: SELECT-then-write under per-key lock; etag column Test files for these drivers are describe.skip; verified compile + build only.
This PR adds atomic compare-and-swap to the driver/storage API via HTTP-style
ifMatch/ifNoneMatchpreconditions onsetItemandsetItemRaw. Useful for atomic swaps or write-once object storage.API
setItem(k, v, { ifMatch?, ifNoneMatch? })returns{ etag }when CAS opts are passed;voidotherwise.getMeta(k)returnsetag?: stringon CAS-aware drivers.CASMismatchErroron precondition failure;CASUnsupportedErrorthrown upfront when CAS opts hit a driver lackingflags.cas(no silent precondition-skip).CASMismatchError.is(err)/err.code === "ERR_CAS_MISMATCH"overinstanceof(driver code and bundled main each carry their own class definition by design — drivers transpile per-file, the rest ofsrcis bundled).Driver support
disposeon eviction)ifNoneMatch:"*"cross-process atomic via temp-file +link();ifMatchsingle-process via per-path mutex (etag =mtime-size-ino). Cross-host needs an external lock.SET ... NXforifNoneMatch:"*";WATCH/MULTI/EXECfor general case; content-hashed etag (SHA-1). No Lua, so works onioredis-mock.kv.atomic().check({ key, versionstamp }).set(...).commit(); versionstamp is the etag. Fast path forifNoneMatch:"*"(single atomiccheck(versionstamp: null)); read-then-atomic for the rest.insertOneforifNoneMatch:"*"(E11000 → mismatch);updateOnewith{ key, _etag: ifMatch }filter forifMatch:<etag>; SHA-1 content etag stored in_etagfield; lazy unique index onkey.If-Match/If-None-Matchheaders; 412/409 → mismatch; etag returned unquoted. Backend support varies (R2 ✓, AWS post-2024 ✓, MinIO version-dependent). aws4fetch signs precondition headers correctly.onlyIf: { etagMatches }/{ etagDoesNotMatch }with"*"wildcard string.put()returningnull→ mismatch. All four CAS modes work in miniflare.onlyIfNew: trueforifNoneMatch:"*",onlyIfMatch: <etag>forifMatch:<etag>;ifMatch:"*"andifNoneMatch:<etag>emulated viagetMetadata+ atomic etag-pinned set. (MockBlobsServerdoesn't echo etag on HEAD — 2 assertions skipped viacasNoMetaEtagflag.)ifMatchonput();ifMatch:"*"andifNoneMatch:<etag>emulated viahead()pre-check + nativeifMatchwrite for atomicity.BlobPreconditionFailedError→CASMismatchError.SELECT etag→checkCAS→INSERT/UPDATE. Newetagcolumn added viaALTER TABLE ADD COLUMN(idempotent). Soft-breaking: existing rows haveNULLetag until rewritten. SHA-1 content etag. Tested on sqlite / libsql / pglite.SELECT etag→checkCAS→INSERT/UPDATE.ALTER TABLE ADD COLUMN etag VARCHAR(64)fired idempotently (try/catch — Vitess has noIF NOT EXISTSforADD COLUMN). On managed Planetscale, run the migration via the schema-deploy workflow.If-Match/If-None-Matchheaders, parsesETagfrom PUT/HEAD responses, maps 412 →CASMismatchError. Server (src/server.ts) parses headers, surfacesCASMismatchError→ 412 andCASUnsupportedError→ 501, setsETagresponse header.flags.casderives dynamically fromlayers[0].flags.cassoCASUnsupportedErroris surfaced upfront when the writable layer lacks CAS.items.create()forifNoneMatch:"*"(409 → mismatch);item.replace(body, { accessCondition: { type: "IfMatch", condition: <etag> } })forifMatch:<etag>(412 → mismatch); read-then-pinned-replace for the other shapes. Etag = the server-managed_etagfield.conditions: { ifMatch, ifNoneMatch }onblockBlobClient.upload(); 412/409 → mismatch. Etag wire-quotes are stripped on the way out, re-added on the way in (matches s3).createEntity()forifNoneMatch:"*";updateEntity(entity, "Replace", { etag })forifMatch:<etag>andifMatch:"*"(SDK accepts"*"as wildcard).ifNoneMatch:<etag>emulated viagetEntity+ conditional update.addConfigurationSettingforifNoneMatch:"*"(409 → mismatch);setConfigurationSetting({ ..., etag }, { onlyIfUnchanged: true })forifMatch:<etag>(412 → mismatch). Other shapes viagetConfigurationSetting+ conditional set with current etag.CASUnsupportedError. Use D1 / Durable Objects / R2 for refs.For drivers without CAS support, callers can fall back to a sidecar lock built on
ifNoneMatch:"*"+ TTL.Test plan
test/drivers/utils.tsgated bysupportsCAS; coversifNoneMatch:"*",ifMatch:<etag>,ifMatch:"*", etag-after-writeflags.casthrowCASUnsupportedErrorpnpm buildcleanDrivers in the ✅ tier with
describe.skiptest files (azure-* and planetscale) are wired and build-verified;supportsCAS: trueis set so the CAS suite engages whenever those tests are un-skipped against real backends.Draft for now — opening for feedback on the API shape, etag-scheme choices per driver, and
flags.cascontract before merge.