npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ozjsey/write-behind

v0.1.2

Published

Write-behind cache in plain TypeScript — local state stays authoritative, the server's reply is discarded on purpose, and a slow response can never clear a newer edit

Readme

@ozjsey/write-behind

Queue writes, coalesce them per key, send them on a clock, retry with backoff — in plain TypeScript with zero dependencies, no peer dependencies and no framework. It runs in the browser, in Node, in a worker.

npm license MIT gzipped 2.48 KiB dependencies 0

See it live — the playground cards drive this engine through its Vue adapter, against a fake server you can break from the card.

The problem

A key is edited while its own save is in flight. This is the case a boolean dirty flag gets wrong:

t=0    user types "hello"      → queued, dirty
t=50   request leaves with "hello"
t=60   user types "hello!"     → still dirty, newer value
t=900  the t=50 response lands → "saved!" → dirty flag cleared

"hello!" is now gone. It is on screen, it looks correct, nothing errored, and the server holds "hello" forever. A slow response cannot clear a newer edit is the bug every naive write-behind ships, it is invisible until a request is slow, and it is the reason this package exists.

The solution

Each key carries a monotonic version, a request records the version it was sent at, and a success clears the key only if the version has not moved. Local state stays authoritative and the network is a background chore. Edit a key ten times and one request goes out carrying the tenth value. The server's reply is discarded on purpose — it can never overwrite the value the user is still typing. A failed save rolls nothing back; the key stays dirty and goes out again next tick, carrying whatever has been typed since.

import { createWriteBehind } from '@ozjsey/write-behind'

const cells: Record<string, string> = { A1: 'foo' }

const wb = createWriteBehind(cells, (value, key) => api.put(`/cell/${key}`, value))

cells.A1 = 'bar'
wb.sync()             // "I changed the record" — diff it and queue what moved
wb.set('A1', 'bar')   // or do both in one call, skipping the diff

sync() is the one thing you have to do. A plain object cannot announce that it changed — there is no proxy here and no observer, which is what keeps this package framework-free — so after you mutate the record, say so. It is idempotent and cheap when nothing has moved: a walk of the record, not a request.

It is a state outbox, not an operation log: keys are independent and last-write-wins. If key b is only valid once key a has landed, this is the wrong tool.

Using Vue? @ozjsey/vue-write-behind is this engine with the sync() call wired to Vue's reactivity and the state mirrored into a reactive store. Everything here still applies; you just never call sync() yourself.

Install

npm install @ozjsey/write-behind

No dependencies and no peer dependencies, and nothing to register. The clock runs in Node exactly as it does in a browser — Node is a target, not a degraded browser.

Usage

A save indicator

const wb = createWriteBehind(cells, (value, key) => api.put(`/cell/${key}`, value))

let last = wb.pending
wb.subscribe(() => {
  if (wb.pending === last) return   // a retry storm changed nothing
  last = wb.pending
  render({ unsaved: wb.pending.length, saving: wb.isSyncing, failing: wb.failed })
})

pending, inFlight, failed and isSyncing are snapshot getters, not live arrays: reading one gives the value as of the last transition, and its identity does not change while its contents do not. So a subscriber can skip a no-op with one !==.

A batch endpoint

One call per tick, every due key in it:

createWriteBehind(cells, {
  flush: (entries) => api.patch('/cells', Object.fromEntries(entries)),
})

Throw or reject and the whole batch stays pending. Resolve with { failed: ['A1'] } to fail part of it — everything else in the batch is treated as written.

Surviving a page refresh

createWriteBehind(cells, (value, key, { final }) =>
  fetch(`/cell/${key}`, {
    method: 'PUT',
    body: JSON.stringify(value),
    keepalive: final,        // the browser finishes this one after the page is gone
  }),
)

When the page goes away this library flushes on visibilitychange → hidden and pagehide, de-duplicated into one — beforeunload is deliberately not used, because mobile browsers routinely discard a page without ever firing it. That flush is forced: it ignores the debounce clock, the retry backoff and retry: false, so it carries the character typed 200 ms before the tab closed. What it cannot do is make the request outlive the page. Only keepalive does that, and the request is yours. flush('unload') says the same thing by hand, for a router leave guard or an Electron close hook.

Everything else

The playground drives this engine through its Vue adapter, against a fake server you can break from the card — every option, every state, in a real browser: the cell does not jump · coalescing · failure never rolls back · retry: false parks a key · pending / inFlight / failed · a batch endpoint · discard() · interval vs debounce · keys · equals, and why in-place mutation is not an edit · flush(), and the flush when the page goes away

ARCHITECTURE.md has the module map, the invariant the split protects, and the three pieces of per-key state that are deliberately kept apart. CHANGELOG.md is the version history.

License

MIT © Ozgur Seyidoglu