{"_id":"@askalf/redisflex","_rev":"8-b5ff3400f99813a40636de3849d89c40","name":"@askalf/redisflex","dist-tags":{"bootstrap":"0.0.0-bootstrap","latest":"0.2.1"},"versions":{"0.0.0-bootstrap":{"name":"@askalf/redisflex","version":"0.0.0-bootstrap","license":"MIT","_id":"@askalf/redisflex@0.0.0-bootstrap","maintainers":[{"name":"askalf","email":"support@askalf.org"}],"homepage":"https://github.com/askalf/redisflex#readme","bugs":{"url":"https://github.com/askalf/redisflex/issues"},"dist":{"shasum":"9c0003db6f912131d2c959fd5d9bd353eafc251a","tarball":"https://registry.npmjs.org/@askalf/redisflex/-/redisflex-0.0.0-bootstrap.tgz","fileCount":2,"integrity":"sha512-OZiIVlQqbMVDJuMrFeAejoHq3+lCcyXpAptep4ceMw0BkbD47sWTdQZ+XdDNbNMCfbMgMWJmsbzqke8sXvIjWw==","signatures":[{"sig":"MEUCIQDhmzDnDWyITLzHwXnnHwqp+O3AspKbJH2sVfyofbQGgwIgKBeCRXzDYRdpIlzNSNOyAPpG3bZV7L+iiiXh9iJB//E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":291},"_npmUser":{"name":"askalf","email":"support@askalf.org"},"deprecated":"bootstrap placeholder; use 0.2.0+","repository":{"url":"git+https://github.com/askalf/redisflex.git","type":"git"},"_npmVersion":"11.19.1","description":"placeholder that creates the package for npm trusted publishing; real releases come from CI with provenance","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/redisflex_0.0.0-bootstrap_1788711727170_0.6306470363950507","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@askalf/redisflex","version":"0.2.0","keywords":["redis","ioredis","in-memory","standalone","bullmq","queue","pubsub","adapter","developer-tools"],"author":{"url":"https://github.com/askalf","name":"askalf"},"license":"MIT","_id":"@askalf/redisflex@0.2.0","maintainers":[{"name":"askalf","email":"support@askalf.org"}],"homepage":"https://github.com/askalf/redisflex","bugs":{"url":"https://github.com/askalf/redisflex/issues"},"dist":{"shasum":"ae1e630c884c9218ed69ef9b3f1e64a53ee6a1bb","tarball":"https://registry.npmjs.org/@askalf/redisflex/-/redisflex-0.2.0.tgz","fileCount":23,"integrity":"sha512-OUTmpWfTEOoucIN407muJ8Zkly5/lCWqyQlLSdrmdpf/RNJ0smM1PhMnajK+xLEF3KkNt+TjjZJ7KUHmx/jUUQ==","signatures":[{"sig":"MEUCIQDeAiFgPhXvNOq9ebS6eANRlCb5rw2+56CuDtdqNphw6gIge3gFIKlmrAel4w+SFC7HZz5o3WVkPjG54RcOZGqu6kE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDhHNZtUCeVSZzSUndTNZYhfTsc3Y/eVdQ91B122wleugIhAOG5IMiMmfLqH+/bCJJyCbGiR4oHOT/k5M85CVva9sWC","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@askalf%2fredisflex@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":132225},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"7da03d058f930d04dd126ae499a93ad49da35fce","scripts":{"test":"node --test test/factory.test.mjs test/memory.test.mjs test/queue.test.mjs","audit":"npm audit --production --audit-level=high","build":"tsc","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a01730d1-5cbf-40be-8b6b-47d5a72cc936"}},"repository":{"url":"git+https://github.com/askalf/redisflex.git","type":"git"},"_npmVersion":"12.0.2","description":"One Redis API. Two modes. ioredis for production, in-process Map+EventEmitter for standalone / dev. Also ships a BullMQ-shaped in-memory queue so you can drop the Redis dep entirely when you don't need it.","directories":{},"_nodeVersion":"22.23.2","dependencies":{"ioredis":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^7.0.2","@types/node":"^26.4.0"},"_npmOperationalInternal":{"tmp":"tmp/redisflex_0.2.0_1788711923650_0.6110241311688627","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"_id":"@askalf/redisflex@0.2.1","bugs":{"url":"https://github.com/askalf/redisflex/issues"},"dist":{"shasum":"e8a626f6b10e2855410da5abffaa4ea85a6aba7e","tarball":"https://registry.npmjs.org/@askalf/redisflex/-/redisflex-0.2.1.tgz","fileCount":23,"integrity":"sha512-/INTG/O5sOeTkye/ZVTioQUJe00TSz08B7p3y584YyNna4uaZYGe5/cQpQLP3AeaEpSlGveWUkdVYTp+lOcF9Q==","signatures":[{"sig":"MEUCIQCv1lBJWw4J1MsMEGGzQPmAZAkHIrBsQv3rYvoBS+UhlwIgKM4wiifepRtvdxWvfb+SggwW2CmmMaqxZPdRZ4EHwRo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDCBezAjHySs+Z7INjxWAhB0pjPGpyJPNf1F4yOq3W8+AiEAz+gMt3PsykU2I5jkDFyH6WN28AOMVIid5VwqcpLStwo="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@askalf%2fredisflex@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":132225},"main":"./dist/index.js","name":"@askalf/redisflex","type":"module","types":"./dist/index.d.ts","author":{"url":"https://github.com/askalf","name":"askalf"},"engines":{"node":">=20.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ae920cfc598e0720508788c2e4d2d4e891d19187","license":"MIT","scripts":{"test":"node --test test/factory.test.mjs test/memory.test.mjs test/queue.test.mjs","audit":"npm audit --production --audit-level=high","build":"tsc","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"version":"0.2.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:a01730d1-5cbf-40be-8b6b-47d5a72cc936"}},"homepage":"https://github.com/askalf/redisflex","keywords":["redis","ioredis","in-memory","standalone","bullmq","queue","pubsub","adapter","developer-tools"],"repository":{"url":"git+https://github.com/askalf/redisflex.git","type":"git"},"_npmVersion":"12.0.2","description":"One Redis API. Two modes. ioredis for production, in-process Map+EventEmitter for standalone / dev. Also ships a BullMQ-shaped in-memory queue so you can drop the Redis dep entirely when you don't need it.","directories":{},"maintainers":[{"name":"askalf","email":"support@askalf.org"}],"_nodeVersion":"22.23.2","dependencies":{"ioredis":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^7.0.2","@types/node":"^26.4.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/redisflex_0.2.1_1789089393073_0.701455788618015"}}},"time":{"created":"2026-09-06T16:22:06.979Z","modified":"2026-09-11T01:16:33.521Z","0.0.2":"2026-05-09T20:06:41.567Z","0.1.0":"2026-06-18T21:05:25.265Z","0.0.0-bootstrap":"2026-09-06T16:22:07.301Z","0.2.0":"2026-09-06T16:25:23.744Z","0.2.1":"2026-09-11T01:16:33.159Z"},"bugs":{"url":"https://github.com/askalf/redisflex/issues"},"author":{"url":"https://github.com/askalf","name":"askalf"},"license":"MIT","homepage":"https://github.com/askalf/redisflex","keywords":["redis","ioredis","in-memory","standalone","bullmq","queue","pubsub","adapter","developer-tools"],"repository":{"url":"git+https://github.com/askalf/redisflex.git","type":"git"},"description":"One Redis API. Two modes. ioredis for production, in-process Map+EventEmitter for standalone / dev. Also ships a BullMQ-shaped in-memory queue so you can drop the Redis dep entirely when you don't need it.","maintainers":[{"name":"askalf","email":"support@askalf.org"}],"readme":"# @askalf/redisflex\n\n> One Redis API. Two modes. Same call sites.\n\nSwitch between **real Redis** (`ioredis`) and **in-process Redis** (Map + EventEmitter) with one line of config. Production runs on real Redis; dev / standalone / \"no-Docker mode\" runs in-process. Same get/set, counters, hashes, lists, sets, sorted sets, pub/sub — drop the Redis server when you don't need it.\n\nAlso ships a **BullMQ-shaped in-memory queue** so you can drop the Redis dep entirely for queueing too.\n\n```bash\nnpm install @askalf/redisflex\n```\n\n[![CI](https://img.shields.io/github/actions/workflow/status/askalf/redisflex/ci.yml?style=flat-square&label=CI&labelColor=020612)](https://github.com/askalf/redisflex/actions)\n[![npm](https://img.shields.io/npm/v/@askalf/redisflex?style=flat-square&color=00ff88&label=npm&labelColor=020612)](https://www.npmjs.com/package/@askalf/redisflex)\n[![License](https://img.shields.io/badge/MIT-00ff88?style=flat-square&label=license&labelColor=020612)](LICENSE)\n\n## Why\n\nMost apps use Redis for three things: cache, pub/sub, and queueing. Most dev environments don't want to spin up a Redis server for any of them. Most CI environments REALLY don't want it.\n\n`redisflex` swaps a real Redis connection for an in-process implementation that speaks the same surface — `get/set`, counters, hashes, lists, sets, sorted sets, pub/sub, expiry, and a sliding-window-rate-limit-shaped Lua eval. Plus a tiny BullMQ-API-compatible queue if you use BullMQ for jobs.\n\nYour call sites stay identical. Flip the mode in config and your app no longer needs Redis to run.\n\n## Use it\n\n### Direct\n\n```ts\nimport { createRedisAdapter } from '@askalf/redisflex';\n\n// Production — real Redis\nconst redis = createRedisAdapter({\n  mode: 'ioredis',\n  url: process.env.REDIS_URL!,\n});\n\n// Dev / standalone — in-process\nconst redis = createRedisAdapter({ mode: 'memory' });\n\nawait redis.set('user:1', 'alice');\nawait redis.publish('events', 'user.created');\n```\n\n### From environment\n\n```ts\nimport { createRedisAdapterFromEnv } from '@askalf/redisflex';\n\n// REDISFLEX_MODE=memory     → in-process\n// otherwise                 → ioredis at $REDIS_URL (or redis://localhost:6379)\nconst redis = createRedisAdapterFromEnv();\n```\n\nCustom env-var names:\n\n```ts\nconst redis = createRedisAdapterFromEnv({\n  modeEnvVar: 'MYAPP_MODE',\n  urlEnvVar: 'MYAPP_REDIS',\n  defaultUrl: null, // null = throw if URL missing, instead of defaulting\n});\n```\n\n### In-memory queue\n\n```ts\nimport { InMemoryQueue, InMemoryWorker } from '@askalf/redisflex';\n\nconst queue = new InMemoryQueue('emails');\nconst worker = new InMemoryWorker(\n  'emails',\n  async (job) => {\n    await sendEmail(job.data);\n  },\n  { queue, concurrency: 4 },\n);\n\nworker.on('completed', (job) => console.log('sent', job.id));\nworker.on('failed', (job, err) => console.error('failed', job.id, err));\nworker.on('drained', () => console.log('all caught up')); // once per drain\n\nawait queue.add('welcome', { to: 'alice@example.com' });\nawait queue.add('reminder', { to: 'bob@example.com' }, {\n  delay: 60_000,\n  attempts: 3,\n  backoff: { delay: 5000 }, // exponential: 5s, 10s, 20s\n});\n\nawait queue.getJobCounts();\n// { delayed: 1, waiting: 0, active: 1, completed: 0, failed: 0 }\n\nawait worker.close(); // waits for in-flight jobs; close(true) skips the wait\n```\n\nThe shape matches BullMQ's `Queue` / `Worker` so you can swap to real BullMQ later by changing imports. Jobs move through the BullMQ states (`delayed → waiting → active → completed`/`failed`; a retrying job counts as `delayed` while its backoff timer is pending) — `getJobs(states)` and `getJobCounts(...states)` report them, and the queue re-emits `completed`/`failed` so you don't need a separate `QueueEvents` object. Completed/failed history is retained for introspection, capped at 1000 each (oldest dropped); set `removeOnComplete`/`removeOnFail` per job to skip retention.\n\n## What's covered\n\n| Family | Operations |\n|---|---|\n| Key/Value | `get`, `set` (incl. trailing `EX <seconds>` / `PX <ms>`), `setex`, `mget`, `del`, `keys`, `exists` |\n| Counters | `incr`, `decr`, `incrby`, `hincrby` |\n| Hashes | `hset`, `hget`, `hgetall`, `hdel` |\n| Lists | `lpush`, `rpush`, `lpop`, `rpop`, `llen`, `lrange` |\n| Sets | `sadd`, `srem`, `smembers`, `sismember`, `scard` |\n| TTL | `expire`, `pexpire`, `ttl`, `pttl` |\n| Sorted Sets | `zadd`, `zcard`, `zscore`, `zrange`, `zrangebyscore`, `zrem`, `zremrangebyscore` (incl. `-inf`/`+inf` and exclusive `(n` bounds) |\n| Pub/Sub | `publish`, `subscribe`, `unsubscribe`, `psubscribe`, `on('message'/'pmessage')` |\n| Scripting | `eval` (sliding-window-rate-limit shape recognized in memory mode) |\n| Lifecycle | `duplicate`, `quit` |\n\nThat's enough surface for cache, pub/sub, BullMQ-style queues, sliding-window rate limits, and most idiomatic Redis usage. Streams, geo, cluster, MULTI/EXEC, bitmap ops aren't covered. Open an issue if you need one — most are mechanical to add.\n\nNote on `set` options: memory mode handles `EX`/`PX` and **throws** on anything else (`NX`, `XX`, `KEEPTTL`, ...) instead of silently ignoring it, so the two modes can't quietly diverge. ioredis mode passes all options through to the server.\n\n## Lua eval in memory mode\n\nMemory mode recognizes the canonical sliding-window-rate-limit Lua script (`ZADD` + `ZREMRANGEBYSCORE` + `ZCARD` + `EXPIRE`, args layout `[key, now, window-ms, member?]`) and returns the post-add count. That's enough for typical rate-limiter use.\n\nAnything else: returns `0` and prints a stderr warning once per process. Pass `{ silentEvalFallback: true }` to suppress the warning. If you need a different Lua script handled natively, open an issue — they're ~5 lines each in `memory-adapter.ts`.\n\n## Pub/Sub: `duplicate()` semantics\n\nReal Redis requires a separate connection for pub/sub vs commands. `IoRedisAdapter.duplicate()` calls `Redis.prototype.duplicate()` — a fresh TCP connection. `MemoryRedisAdapter.duplicate()` returns a new adapter that **shares** the underlying EventEmitter + state, so pub/sub fans out the same way real Redis does.\n\n## What it isn't\n\n- **Not a cluster client.** `ioredis` mode supports cluster URLs; memory mode is single-node by definition.\n- **Not durable.** Memory mode loses everything on process restart. Persistence is your application's problem (database run table, snapshot to disk, etc.).\n- **Not a full Lua interpreter.** See above.\n- **Not a substitute for real Redis under load.** The data structures are correct but not optimized for high throughput. Use real Redis in production.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Also by askalf\n\n| Project | What it does |\n|---------|-------------|\n| [browser-bridge](https://github.com/askalf/browser-bridge) | Stealth headless Chromium in a container. CDP on 9222 — Playwright/Puppeteer/MCP-compatible. |\n| [dario](https://github.com/askalf/dario) | Local LLM router. Use your Claude Max/Pro subscription as an API. |\n| [pgflex](https://github.com/askalf/pgflex) | One Postgres API. Two modes (real PG ↔ PGlite WASM). |\n\n\n---\n\n## Built by Sprayberry Labs\n\nThis is one of the open-source building blocks from **[Sprayberry Labs](https://sprayberrylabs.com)** — an independent studio (Atlanta, GA) that ships bespoke software and **fixed-price code & security audits**, delivered with the AI workforce these tools are part of.\n\nPart of the [askalf](https://askalf.org) ecosystem — a self-hosted AI workforce platform, now in early access.\n\n**Got a codebase that needs an expert read?** → **[Scan a repo — free mini-audit](https://sprayberrylabs.com)**, or see the **$1,500 fixed-price Audit** and build Sprints. · [sprayberrylabs.com](https://sprayberrylabs.com) · hello@sprayberrylabs.com\n\n---\nPart of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.\n","readmeFilename":"README.md"}