Skip to content
Merged

Dev #20

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,46 @@ jobs:
- name: Run browser tests
run: wasm-pack test --headless --chrome wasm-edge

# Builds the npm package the way the release does, packs a tarball, and
# imports it from outside the working tree. Typecheck and unit tests both
# pass against a tree where `pkg/snippets/` sits on disk, so neither can see
# that it never ships — which is how 0.1.3 through 0.3.0 all published a
# package that threw ERR_MODULE_NOT_FOUND on import.
npm-package:
name: Package Installability (recached-edge)
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: "recached-wasm-cache"

- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
# Pinned for the same reason as browser-tests above.
version: 'v0.15.0'

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"

- name: Build the package
run: cd wasm-edge && npm install && npm run build

- name: Verify the tarball is installable
run: cd wasm-edge && npm run verify

integration-load-chaos:
name: Load & Chaos Tests
runs-on: ubuntu-latest
Expand Down
37 changes: 29 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,32 +207,53 @@ jobs:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"

# `--out-name recached_edge` is not cosmetic: sdk.ts imports
# `./pkg/recached_edge.js`, and the crate is named wasm-edge, so the
# default output (wasm_edge.js) would not match the import.
- name: Build Wasm package
run: wasm-pack build wasm-edge --target web --release --out-name recached-edge
run: wasm-pack build wasm-edge --target web --release --out-name recached_edge

# wasm-pack writes pkg/.gitignore containing "*". npm honours a nested
# .gitignore even inside a directory listed in "files", so leaving it
# here publishes an SDK with no WebAssembly in it.
- name: Drop wasm-pack's pkg/.gitignore
run: rm -f wasm-edge/pkg/.gitignore

- name: Build the TypeScript SDK
# The published entrypoint is sdk.js — the `createCache`/`Cache` API every
# doc example and both framework SDKs import. Releases up to 0.3.0
# published wasm-edge/pkg instead, which exports only the low-level
# `RecachedCache` binding, so the documented API was never on npm at all.
run: cd wasm-edge && npm install && npx tsc

- name: Copy LICENSE + NOTICE into package
run: cp LICENSE.md NOTICE wasm-edge/pkg/
run: cp LICENSE.md NOTICE wasm-edge/

- name: Set package name and version
# wasm-pack derives the npm name and version from the crate — patch both so the
# published package always matches the git tag (e.g. v0.1.5 → 0.1.5).
- name: Set package version
# The npm name lives in wasm-edge/package.json already; only the version
# is patched, so it always matches the git tag (e.g. v0.3.1 → 0.3.1).
run: |
node -e "
const fs = require('fs');
const path = 'wasm-edge/pkg/package.json';
const path = 'wasm-edge/package.json';
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
pkg.name = 'recached-edge';
pkg.version = process.env.TAG_VERSION.replace(/^v/, '');
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
console.log('Package:', pkg.name, pkg.version);
"
env:
TAG_VERSION: ${{ github.ref_name }}

# Packs a real tarball and imports it from outside the tree: proves the
# snippets/ glue resolves and that `createCache` is actually exported.
# Every release from 0.1.3 to 0.3.0 failed both and shipped anyway.
- name: Verify the package is installable
run: cd wasm-edge && npm run verify

- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: cd wasm-edge/pkg && npm publish --access public
run: cd wasm-edge && npm publish --access public

npm-sdks:
name: Publish @recached/${{ matrix.package }} to NPM
Expand Down
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,90 @@ All notable changes to Recached are documented here.

---

## [0.3.1] — 2026-08-06

A packaging-only release. No engine, server or SDK behaviour changed — but every
`recached-edge` version before this one was impossible to install, so in practice this is the
first usable browser release.

### Fixed — the npm package could not be imported (0.1.3 – 0.3.0)

- **`snippets/` was never published.** The wasm-bindgen glue opens with
`import { openRecachedDb, … } from './snippets/<crate-hash>/inline0.js'` — the IndexedDB
helpers behind `enable_persistence`. That directory was in neither the tarball nor the `files`
array wasm-pack generates, so `npm install recached-edge` followed by any import died with
`ERR_MODULE_NOT_FOUND` before a line of application code ran. It bundles statically, so
webpack, Turbopack and Vite all failed at build time, not at runtime. Fourteen consecutive
releases shipped this way: the break dates from 0.1.3, when the IndexedDB helpers were added.

- **The documented API was not the published one.** The release workflow published
`wasm-edge/pkg` — raw wasm-pack output, whose only exports are `RecachedCache`, `initSync` and
a default init. `createCache`, `Cache`, `onMutation`, `getJSON` and the ref-counted `liveQuery`
live in `sdk.js`, which was never published. Every example in the README and docs imported a
symbol that did not exist on npm, and both framework SDKs peer-depend on it. The release now
publishes the `wasm-edge` package itself, with `pkg/` nested inside it.

- **`sdk.ts` imported a filename no build produced.** It loads `./pkg/recached_edge.js`, but the
crate is named `wasm-edge` (default output `wasm_edge.js`) and the release built with
`--out-name recached-edge` (output `recached-edge.js`). Both build paths now pass
`--out-name recached_edge`, matching the import and the stub CI already generated.

- **`pkg/.gitignore` would have silently emptied the fixed package.** wasm-pack writes one
containing `*`, and npm applies a nested `.gitignore` even to a directory listed in `files` —
so simply switching the publish directory would have shipped an SDK with no WebAssembly in it.
`npm run build:wasm` deletes it, and the release deletes it again before packing.

### Added — a release gate that would have caught all of the above

- **`wasm-edge/scripts/verify-package.mjs`**, run by a new `npm-package` CI job on every push and
by the release workflow immediately before `npm publish`. It packs a real tarball, extracts it
outside the working tree, walks the import graph from `sdk.js` through the glue to `snippets/`,
and imports the result in Node to assert `createCache`, `Cache` and `init` are exported. It
also runs from `prepack`, so a manual `npm publish` cannot bypass it.

The gap this closes: typecheck, unit tests and `wasm-pack test` all pass against a working tree
where `snippets/` is present on disk. None of them can observe what `files` excludes. Only
packing and importing from outside the tree can, and nothing did that.

- `LICENSE.md` and `NOTICE` now ship inside the npm package (`files`), and the meaningless
`licenseFile: "../LICENSE.md"` key — which pointed outside the package — was dropped.

### Documentation — running the client with no server

Local-only mode was supported in code and mentioned in passing, but never documented as a mode with
edges. It now is, because "can I use this as a client cache without running the server?" has a
sharper answer than the docs were giving:

- **New [use case: no server at all](docs/guide/use-cases.md)** — what works standalone, and a table
of what is *inert* rather than broken. `publish`/`subscribe`/`onMessage`, `liveQuery`,
`syncToken`/`syncScopes` and `pendingWrites`/`onOutboxFull` do not throw without a connection; they
silently do nothing, and pub/sub in particular does **not** fall back to BroadcastChannel. Also
states where local-only Recached is the wrong choice: against React Query or SWR for a plain
request cache, a ~550 KB `.wasm` buys Redis semantics and nothing else.
- **The "one-sentence test"** framed Recached as pointless unless a client reads backend-written
data. It now names the third answer — no server in the picture at all.
- **[Getting Started (Browser)](docs/browser/getting-started.md)** gained the same works/inert split
next to its local-only example.
- **Known wart, now written down:** with `persistence: true` and no `connect`, every write still
records an IndexedDB outbox row for a replay that cannot happen, and warns `offline write queue
full` past 10,000 writes. Nothing is lost; it is wasted I/O and a misleading message.
(`wasm-edge/src/lib.rs` — `queue_write` skips the outbox only when there is neither a URL *nor*
persistence.)
- **The Next.js (App Router) example was a no-op** — a provider that returned its children untouched
and imported a symbol it never used. Replaced with the real client-only pattern: `RecachedProvider`
in a `'use client'` boundary, plus the plain `useEffect` version. Documents that the provider
renders `null` until `createCache()` resolves in an effect, so wrapping the whole app opts the page
out of SSR.

### Changed

- `@recached/react` and `@recached/vue` raise their `recached-edge` peer floor from `>=0.1.4` to
`>=0.3.1`. The old range was satisfiable only by versions that cannot be imported.
- `wasm-edge/tsconfig.json` adds `ESNext.Disposable` to `lib`: wasm-bindgen 0.2.120 emits
`[Symbol.dispose]()` on the generated class, which ES2020's lib does not declare.

---

## [0.3.0] — 2026-08-03

### Added
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ resolver = "2"
# ── Single source of truth for all crate versions ────────────────────────────
# Members inherit with: version.workspace = true / edition.workspace = true
[workspace.package]
version = "0.3.0"
version = "0.3.1"
edition = "2024"
license = "Apache-2.0"
authors = ["ThinkGrid Labs"]
Expand Down
6 changes: 3 additions & 3 deletions Formula/recached.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Recached < Formula
desc "Blazing fast, multi-core drop-in replacement for Redis"
homepage "https://github.com/recached-dev/recached"
version "0.3.0"
version "0.3.1"
license "Apache-2.0"

# The checksums below are placeholders until the v0.2.4 release artifacts
Expand All @@ -16,11 +16,11 @@ class Recached < Formula
# placeholder makes brew fail loudly, which is the far better failure.
on_macos do
on_intel do
url "https://github.com/recached-dev/recached/releases/download/v0.3.0/recached-macos-amd64"
url "https://github.com/recached-dev/recached/releases/download/v0.3.1/recached-macos-amd64"
sha256 "REPLACE_WITH_AMD64_SHA256"
end
on_arm do
url "https://github.com/recached-dev/recached/releases/download/v0.3.0/recached-macos-arm64"
url "https://github.com/recached-dev/recached/releases/download/v0.3.1/recached-macos-arm64"
sha256 "REPLACE_WITH_ARM64_SHA256"
end
end
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ npm install recached-edge
```

> [!IMPORTANT]
> **0.3.0 is the first stable release.** Install `recached-edge@^0.3.0`; earlier versions are not
> recommended.
> **Install `recached-edge@^0.3.1`.** Every published version from 0.1.3 to 0.3.0 shipped without
> wasm-pack's `snippets/` directory and failed to import at all; 0.3.1 is the first release that
> installs from npm. See the [changelog](CHANGELOG.md) for details.

---

Expand Down Expand Up @@ -96,11 +97,23 @@ password, no TLS, and no restriction on which web pages may open the sync socket
`6379` and `6380` are defaults, not fixtures — set `RECACHED_PORT` and `RECACHED_WS_PORT` (plus
`RECACHED_METRICS_PORT`) to move them, which is also what running two instances on one host takes.

**The browser half also runs alone.** Drop `connect` and `recached-edge` never opens a socket — the
same engine runs in WASM as a standalone client cache with TTLs, counters, JSON documents, glob
queries, IndexedDB persistence and cross-tab sync, with no Recached server and no backend changes:

```typescript
const cache = await createCache({ persistence: true, broadcastChannel: 'my-app' });
cache.setJSON('user:42', user, 60); // expires on its own, survives a refresh
```

What you give up is what needs a peer: pub/sub, live queries and cross-device sync. See
[use cases: no server at all](https://recached.dev/guide/use-cases#no-server-at-all).

---

## Benchmarks

Measured with `redis-benchmark` (100k requests, 50 connections, 64-byte values, randomized keys, persistence disabled on all servers) on a 4-core Intel i5-8259U laptop, July 2026 — Recached v0.1.8 vs Redis 7.2.5 vs Valkey 9.1.0, one server at a time. Current release is v0.3.0; these command paths were A/B tested across the v0.2.4 changes and spot-checked again on v0.3.0 (SET 455k, GET 518k, INCR 526k pipelined on the same laptop), moving within run-to-run noise each time — but the three-way suite has not been re-run since v0.1.8.
Measured with `redis-benchmark` (100k requests, 50 connections, 64-byte values, randomized keys, persistence disabled on all servers) on a 4-core Intel i5-8259U laptop, July 2026 — Recached v0.1.8 vs Redis 7.2.5 vs Valkey 9.1.0, one server at a time. Current release is v0.3.1 (packaging only — no engine change since v0.3.0); these command paths were A/B tested across the v0.2.4 changes and spot-checked again on v0.3.0 (SET 455k, GET 518k, INCR 526k pipelined on the same laptop), moving within run-to-run noise each time — but the three-way suite has not been re-run since v0.1.8.

Pipelined (`-P 16`) — raw command throughput, requests/sec, **bold** = best per row:

Expand Down
Loading
Loading