@desmat/redis-store 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,8 @@ Leans into Redis’ strong suits to bring relational aspects to the simple but p
12
12
 
13
13
  Plays well with Upstash and Vercel but will work with any Redis instance via REST API.
14
14
 
15
+ Also ships with `MemoryStore`, a pure in-memory implementation of `RedisStore` — a drop-in swap for local development and tests that don't need (or want) a live Redis instance. See [In-memory store](#in-memory-store) below.
16
+
15
17
 
16
18
  ## Installation
17
19
 
@@ -171,3 +173,50 @@ await Promise.all([
171
173
  // ZREM testthings:user:<USER_UUID> <UUID>
172
174
  // ...
173
175
  ```
176
+
177
+
178
+ ## In-memory store
179
+
180
+ `MemoryStore<T>` implements the exact same `Store<T>` interface as `RedisStore<T>` but keeps everything in plain JS `Map`s — no Redis connection, no environment variables, no network calls. Application code written against `Store<T>` can swap between the two without any changes.
181
+
182
+ ```typescript
183
+ import { MemoryStore } from '@desmat/redis-store';
184
+
185
+ const store = {
186
+ users: new MemoryStore<User>({ key: "user" }),
187
+ things: new MemoryStore<Thing>({ key: "thing", options: ThingOptions }),
188
+ };
189
+
190
+ const user = await store.users.create({ name: "User One" });
191
+ const things = await store.things.find({ user: user.id });
192
+ ```
193
+
194
+ Useful for:
195
+ - Local development without provisioning a real Redis instance
196
+ - Test suites that need fast, isolated, disposable data (each `MemoryStore` instance is its own private state — nothing persists between processes, and nothing is shared unless you share the instance)
197
+
198
+ Constructor accepts an optional `seed` array to pre-populate records (and their lookup indices) at construction time, so seeded data is immediately queryable via `find`/`ids` without needing to call `.create()` for each one:
199
+
200
+ ```typescript
201
+ const things = new MemoryStore<Thing>({
202
+ key: "thing",
203
+ options: ThingOptions,
204
+ seed: [
205
+ { id: "thing-1", createdAt: Date.now(), createdBy: "user-1", label: "Seeded thing" },
206
+ ],
207
+ });
208
+ ```
209
+
210
+ Notes on parity with `RedisStore`:
211
+ - `create`/`update`/`delete`/lookup/counter semantics are ported 1:1, including Redis's rank-based (not score-range) windowing for `count`/`offset` in `ids()`/`find()`, and `queryCounter`'s lexicographic range-bound behavior for `incCounters`/`queryCounter`.
212
+ - `options.expire` is a no-op (data is ephemeral for the life of the process anyway).
213
+ - There's no equivalent to `RedisStore`'s raw `.redis` client escape hatch — code that reaches through it directly for one-off scripts (rather than going through the `Store<T>` methods) isn't portable to `MemoryStore`.
214
+
215
+
216
+ ## Testing
217
+
218
+ ```bash
219
+ npm test
220
+ ```
221
+
222
+ Runs the `MemoryStore` unit test suite (`test/*.test.ts`) via Node's built-in test runner (through `tsx`) — no live Redis needed. `RedisStore` itself has no automated tests; it's a thin wrapper around `@upstash/redis`.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Redis } from "@upstash/redis";
2
+ import type { Store } from "./store";
2
3
  export type RedisStoreRecord = {
3
4
  id: string;
4
5
  createdAt: number;
5
6
  updatedAt?: number;
6
7
  deletedAt?: number;
7
8
  };
8
- export default class RedisStore<T extends RedisStoreRecord> {
9
+ export { Store } from "./store";
10
+ export { default as MemoryStore } from "./memory-store";
11
+ export default class RedisStore<T extends RedisStoreRecord> implements Store<T> {
9
12
  redis: Redis;
10
13
  key: string;
11
14
  setKey: string;
@@ -37,7 +40,7 @@ export default class RedisStore<T extends RedisStoreRecord> {
37
40
  lookups?: any;
38
41
  }): Promise<T>;
39
42
  update(value: any, options?: any): Promise<T>;
40
- incrementCounters(values: Record<string, string | number>, delta: {
43
+ incCounters(values: Record<string, string | number>, delta: {
41
44
  total: number;
42
45
  count: number;
43
46
  }): Promise<any>;
package/dist/index.js CHANGED
@@ -19,9 +19,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  return (mod && mod.__esModule) ? mod : { "default": mod };
20
20
  };
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MemoryStore = void 0;
22
23
  const moment_1 = __importDefault(require("moment"));
23
24
  const utils_1 = require("@desmat/utils");
24
25
  const redis_1 = require("@upstash/redis");
26
+ var memory_store_1 = require("./memory-store");
27
+ Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return __importDefault(memory_store_1).default; } });
25
28
  // polyfill Set.intersection
26
29
  ;
27
30
  (function () {
@@ -260,10 +263,16 @@ class RedisStore {
260
263
  this.debug && console.log(`RedisStore<${this.key}>.update`, { response });
261
264
  return updatedValue;
262
265
  }
263
- async incrementCounters(values, delta) {
266
+ async incCounters(values, delta) {
264
267
  var _a;
265
- this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { values, delta });
268
+ this.debug && console.log(`RedisStore<${this.key}>.incCounters`, { values, delta });
266
269
  const counters = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.counters) || [];
270
+ counters
271
+ .filter((counter) => !counter.split(":").every((d) => typeof values[d] != "undefined"))
272
+ .forEach((counter) => {
273
+ const missing = counter.split(":").filter((d) => typeof values[d] == "undefined");
274
+ console.warn(`RedisStore<${this.key}>.incCounters WARNING: skipping counter "${counter}": missing dimension(s) ${missing.join(", ")} in values`, { values });
275
+ });
267
276
  const responses = await Promise.all(counters
268
277
  .filter((counter) => counter.split(":").every((d) => typeof values[d] != "undefined"))
269
278
  .flatMap((counter) => {
@@ -273,7 +282,7 @@ class RedisStore {
273
282
  this.redis.zincrby(`${this.key}Counts:${counter}`, delta.count, member),
274
283
  ];
275
284
  }));
276
- this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { responses });
285
+ this.debug && console.log(`RedisStore<${this.key}>.incCounters`, { responses });
277
286
  return responses;
278
287
  }
279
288
  async queryCounter(kind, counter, exact, range) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desmat/redis-store",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "build": "tsc",
16
16
  "prepare": "tsc",
17
- "example": "ts-node --skipProject ./src/example.ts"
17
+ "example": "ts-node --skipProject ./src/example.ts",
18
+ "test": "tsx --test test/**/*.test.ts"
18
19
  },
19
20
  "repository": {
20
21
  "type": "git",
@@ -36,6 +37,7 @@
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/node": "^22.8.4",
40
+ "tsx": "^4.21.0",
39
41
  "typescript": "^4.0.0"
40
42
  }
41
43
  }