Skip to content

Helpers

Eugene Lazutkin edited this page Jul 1, 2026 · 2 revisions

Helpers

Higher-level glue built on the core verbs. Currently this is a single helper, io.update, which turns a read-modify-write into a safe conditional PUT with automatic conflict retry.

import io from 'double-meh';

// bump a counter without clobbering a concurrent writer
const updated = await io.update('https://example.com/u/1', cur => ({
  ...cur,
  n: cur.n + 1
}));

io.update(target, fn, options?)

An optimistic-concurrency read-modify-write. update:

  1. GETs the current representation with its envelope (cache bypassed), capturing the ETag.
  2. Calls fn(data) to produce the next value (fn may be async).
  3. If fn returns undefined, nothing is written — the current data is returned as-is.
  4. Otherwise PUTs the result with If-Match: <etag>, so the server rejects the write if the resource changed underneath you.
  5. On success, invalidates the target's cache entry (when the cache is active) and returns the PUT result.

If the PUT comes back 412 Precondition Failed, update re-reads the resource, re-applies fn to the fresh data, and retries — up to 3 conflict retries before the BadStatus error is rethrown. Any non-412 error propagates immediately.

// read-modify-write: append a tag, retrying if someone else writes first
const result = await io.update('/api/doc/42', doc => ({
  ...doc,
  tags: [...doc.tags, 'urgent']
}));

Return undefined from fn to abort the write conditionally — a convenient no-op when the current state already satisfies you:

await io.update('/api/doc/42', doc =>
  doc.archived ? undefined : {...doc, archived: true}
);

options (third argument) is forwarded to both the GET and the PUT, so you can pass headers, query, signal, and other per-request settings. Note that cache: false (on the read) and ifMatch (on the write) are set for you.

target may be a URL string, a URL, or an options object with a url — the same shapes the core verbs accept.

See also

Clone this wiki locally