@nymbalhq/doc-store-redis 1.1.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -23,6 +23,11 @@ declare class RedisDocumentStore implements DocumentStoreAdapter {
23
23
  query<T>(collection: string, params: QueryParams): Promise<QueryResult<T>>;
24
24
  batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>>;
25
25
  batchPut<T>(collection: string, items: Map<string, T>): Promise<void>;
26
+ /**
27
+ * Writes the new contents to a staging hash under the prefix, then swaps it in with RENAME in one MULTI, dropping the
28
+ * collection's expiry index with it (the new documents have no TTL). A source that throws deletes the staging hash.
29
+ */
30
+ replaceCollection<T>(collection: string, entries: AsyncIterable<[string, T]>): Promise<void>;
26
31
  /** Removes every document whose TTL has passed. Reads never return them in the meantime. */
27
32
  sweep(): Promise<void>;
28
33
  close(): Promise<void>;
@@ -32,4 +37,5 @@ declare function createRedisDocumentStore(options: RedisDocumentStoreOptions): P
32
37
  /** The kernel's entry: `infrastructure.documentStore: 'redis'`, with `{ url, prefix }` from the `redis` block. */
33
38
  declare function createAdapter(ctx: AdapterContext): Promise<DocumentStoreAdapter>;
34
39
 
35
- export { RedisDocumentStore, type RedisDocumentStoreOptions, createAdapter, createRedisDocumentStore };
40
+ export { RedisDocumentStore, createAdapter, createRedisDocumentStore };
41
+ export type { RedisDocumentStoreOptions };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { ConfigError } from "@nymbalhq/types";
3
3
 
4
4
  // src/redis-document-store.ts
5
+ import { randomUUID } from "crypto";
5
6
  import { Redis } from "ioredis";
6
7
  import {
7
8
  AdapterError,
@@ -58,6 +59,7 @@ end
58
59
  return #due
59
60
  `;
60
61
  var SWEEP_BATCH = 500;
62
+ var REPLACE_BATCH = 500;
61
63
  var RedisDocumentStore = class {
62
64
  #client;
63
65
  #url;
@@ -143,6 +145,39 @@ var RedisDocumentStore = class {
143
145
  for (const [key, doc] of items) fields.push(key, JSON.stringify({ d: doc, e: null }));
144
146
  await this.#client.multi().hset(this.#docs(collection), ...fields).zrem(this.#expiry(collection), ...items.keys()).exec();
145
147
  }
148
+ /**
149
+ * Writes the new contents to a staging hash under the prefix, then swaps it in with RENAME in one MULTI, dropping the
150
+ * collection's expiry index with it (the new documents have no TTL). A source that throws deletes the staging hash.
151
+ */
152
+ async replaceCollection(collection, entries) {
153
+ const staging = `${this.#prefix}:stage:${randomUUID()}:doc:${collection}`;
154
+ let staged = 0;
155
+ try {
156
+ let fields = [];
157
+ const flush = async () => {
158
+ if (fields.length === 0) return;
159
+ await this.#client.hset(staging, ...fields);
160
+ fields = [];
161
+ };
162
+ for await (const [key, doc] of entries) {
163
+ fields.push(key, JSON.stringify({ d: doc, e: null }));
164
+ staged += 1;
165
+ if (fields.length >= REPLACE_BATCH * 2) await flush();
166
+ }
167
+ await flush();
168
+ } catch (error) {
169
+ await this.#client.del(staging).catch(
170
+ (err) => this.#logger?.warn({ err, staging }, "redis document store: a staging hash was left")
171
+ );
172
+ throw error;
173
+ }
174
+ const swap = this.#client.multi();
175
+ if (staged > 0) swap.rename(staging, this.#docs(collection));
176
+ else swap.del(this.#docs(collection));
177
+ const results = await swap.del(this.#expiry(collection)).exec();
178
+ const failed = results?.find(([err]) => err !== null)?.[0];
179
+ if (failed) throw failed;
180
+ }
146
181
  /** Removes every document whose TTL has passed. Reads never return them in the meantime. */
147
182
  async sweep() {
148
183
  try {
@@ -209,7 +244,12 @@ function pingRedis(url, timeoutMs = HEALTH_CHECK_TIMEOUT_MS) {
209
244
  ).finally(() => client.disconnect());
210
245
  }
211
246
  async function connectRedis(url, logger) {
212
- const client = new Redis(url, { lazyConnect: true });
247
+ const client = new Redis(url, {
248
+ lazyConnect: true,
249
+ maxRetriesPerRequest: 1,
250
+ retryStrategy: (times) => Math.min(times * 200, 1e3),
251
+ connectTimeout: 2e3
252
+ });
213
253
  client.on("error", (err) => logger?.warn({ err }, "redis connection error"));
214
254
  try {
215
255
  await client.connect();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/redis-document-store.ts"],"sourcesContent":["import { ConfigError, type AdapterContext, type DocumentStoreAdapter } from '@nymbalhq/types'\nimport {\n createRedisDocumentStore,\n RedisDocumentStore,\n type RedisDocumentStoreOptions,\n} from './redis-document-store.js'\n\nexport { createRedisDocumentStore, RedisDocumentStore, type RedisDocumentStoreOptions }\n\n/** The kernel's entry: `infrastructure.documentStore: 'redis'`, with `{ url, prefix }` from the `redis` block. */\nexport async function createAdapter(ctx: AdapterContext): Promise<DocumentStoreAdapter> {\n if (ctx.slot !== 'documentStore') {\n throw new ConfigError(`@nymbalhq/doc-store-redis serves documentStore, not ${ctx.slot}`, {\n code: 'adapter.wrong_slot',\n context: { slot: ctx.slot },\n })\n }\n const { url, prefix } = ctx.config\n if (typeof url !== 'string' || url === '' || typeof prefix !== 'string' || prefix === '') {\n throw new ConfigError('@nymbalhq/doc-store-redis needs a Redis url and a key prefix', {\n code: 'config.invalid',\n context: { slot: ctx.slot },\n })\n }\n return createRedisDocumentStore({ url, prefix, logger: ctx.logger })\n}\n","import { Redis } from 'ioredis'\nimport {\n AdapterError,\n boundedHealthCheck,\n connectionSecrets,\n HEALTH_CHECK_TIMEOUT_MS,\n queryDocuments,\n type DocumentStoreAdapter,\n type HealthReport,\n type Logger,\n type PutOptions,\n type QueryParams,\n type QueryResult,\n} from '@nymbalhq/types'\n\nexport interface RedisDocumentStoreOptions {\n /** `redis://[user:password@]host:port[/db]`; the db index in the path is honoured. */\n url: string\n /** Namespaces every key the store writes: `<prefix>:doc:<collection>` and its expiry index. */\n prefix: string\n logger?: Logger\n /** How often expired documents are removed (they are never returned once expired). 0 turns the sweep off. */\n sweepIntervalMs?: number\n now?: () => number\n}\n\n/** A stored document and when it expires (epoch ms), or null for never. */\ninterface Stored {\n d: unknown\n e: number | null\n}\n\n// Writes a document and keeps the collection's expiry index in step with it.\n// KEYS: documents hash, expiry zset, expiring-collections set. ARGV: key, stored JSON, expiresAt or '', collection.\nconst PUT = `\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\nif ARGV[3] == '' then\n redis.call('ZREM', KEYS[2], ARGV[1])\nelse\n redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])\n redis.call('SADD', KEYS[3], ARGV[4])\nend\nreturn 1\n`\n\n// The same write, only when the live document's field is strictly equal to the expected value (JavaScript `===`:\n// objects and arrays never are). ARGV[5]: the field; ARGV[6]: the expected value as JSON, or '' for undefined;\n// ARGV[7]: now (epoch ms).\nconst PUT_IF = `\nlocal current = redis.call('HGET', KEYS[1], ARGV[1])\nif not current then return 'absent' end\nlocal stored = cjson.decode(current)\nif stored.e ~= cjson.null and tonumber(stored.e) <= tonumber(ARGV[7]) then return 'absent' end\nlocal actual = nil\nif type(stored.d) == 'table' then actual = stored.d[ARGV[5]] end\nif ARGV[6] == '' then\n if actual ~= nil then return 'mismatch' end\nelse\n local expected = cjson.decode(ARGV[6])\n if type(expected) == 'table' or type(actual) == 'table' then return 'mismatch' end\n if actual ~= expected then return 'mismatch' end\nend\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\nif ARGV[3] == '' then\n redis.call('ZREM', KEYS[2], ARGV[1])\nelse\n redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])\n redis.call('SADD', KEYS[3], ARGV[4])\nend\nreturn 'ok'\n`\n\n// Removes up to ARGV[2] documents whose expiry has passed at ARGV[1], checking each again under the script, so a\n// document written again since it was indexed is kept.\nconst SWEEP = `\nlocal due = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[1], 'LIMIT', 0, tonumber(ARGV[2]))\nfor _, key in ipairs(due) do\n local current = redis.call('HGET', KEYS[1], key)\n if current then\n local stored = cjson.decode(current)\n if stored.e ~= cjson.null and tonumber(stored.e) <= tonumber(ARGV[1]) then\n redis.call('HDEL', KEYS[1], key)\n end\n end\n redis.call('ZREM', KEYS[2], key)\nend\nreturn #due\n`\n\ntype Script = (...args: Array<string | number>) => Promise<unknown>\ninterface Scripted {\n nymbalPut: Script\n nymbalPutIf: Script\n nymbalSweep: Script\n}\n\nconst SWEEP_BATCH = 500\n\n/** A document store on Redis: one hash per collection under the prefix, ordered and filtered by the reference. */\nexport class RedisDocumentStore implements DocumentStoreAdapter {\n readonly #client: Redis & Scripted\n readonly #url: string\n readonly #prefix: string\n readonly #now: () => number\n readonly #logger: Logger | undefined\n #sweepTimer: NodeJS.Timeout | null = null\n\n constructor(client: Redis, options: RedisDocumentStoreOptions) {\n client.defineCommand('nymbalPut', { numberOfKeys: 3, lua: PUT })\n client.defineCommand('nymbalPutIf', { numberOfKeys: 3, lua: PUT_IF })\n client.defineCommand('nymbalSweep', { numberOfKeys: 2, lua: SWEEP })\n this.#client = client as Redis & Scripted\n this.#url = options.url\n this.#prefix = options.prefix\n this.#now = options.now ?? Date.now\n this.#logger = options.logger\n const interval = options.sweepIntervalMs ?? 60_000\n if (interval > 0) {\n this.#sweepTimer = setInterval(() => void this.sweep(), interval)\n this.#sweepTimer.unref()\n }\n }\n\n capabilities(): readonly string[] {\n return ['get', 'put', 'delete', 'query', 'batch', 'ttl', 'conditional-put']\n }\n\n healthCheck(): Promise<HealthReport> {\n return pingRedis(this.#url)\n }\n\n async get<T>(collection: string, key: string): Promise<T | null> {\n const raw = await this.#client.hget(this.#docs(collection), key)\n return this.#live<T>(raw)\n }\n\n async put<T>(\n collection: string,\n key: string,\n document: T,\n options: PutOptions = {},\n ): Promise<void> {\n const expiresAt = options.ttl !== undefined ? this.#now() + options.ttl * 1000 : null\n const stored = JSON.stringify({ d: document, e: expiresAt } satisfies Stored)\n const keys = [this.#docs(collection), this.#expiry(collection), this.#expiring()]\n const args = [key, stored, expiresAt === null ? '' : String(expiresAt), collection]\n if (options.conditionKey === undefined) {\n await this.#client.nymbalPut(...keys, ...args)\n return\n }\n const expected =\n options.conditionValue === undefined ? '' : JSON.stringify(options.conditionValue)\n const outcome = await this.#client.nymbalPutIf(\n ...keys,\n ...args,\n options.conditionKey,\n expected,\n this.#now(),\n )\n if (outcome !== 'ok') {\n throw new AdapterError(\n 'condition-failed',\n outcome === 'absent'\n ? `Condition failed on ${collection}/${key}: document does not exist`\n : `Condition failed on ${collection}/${key}: ${options.conditionKey} mismatch`,\n { context: { collection, key, conditionKey: options.conditionKey } },\n )\n }\n }\n\n async delete(collection: string, key: string): Promise<void> {\n await this.#client\n .multi()\n .hdel(this.#docs(collection), key)\n .zrem(this.#expiry(collection), key)\n .exec()\n }\n\n async query<T>(collection: string, params: QueryParams): Promise<QueryResult<T>> {\n const all = await this.#client.hgetall(this.#docs(collection))\n const live: Array<[string, unknown]> = []\n for (const [key, raw] of Object.entries(all)) {\n const stored = this.#decode(raw)\n if (stored !== null) live.push([key, stored.d])\n }\n return queryDocuments<T>(live, params)\n }\n\n async batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>> {\n const result = new Map<string, T>()\n if (keys.length === 0) return result\n const values = await this.#client.hmget(this.#docs(collection), ...keys)\n keys.forEach((key, i) => {\n const doc = this.#live<T>(values[i] ?? null)\n if (doc !== null) result.set(key, doc)\n })\n return result\n }\n\n async batchPut<T>(collection: string, items: Map<string, T>): Promise<void> {\n if (items.size === 0) return\n const fields: string[] = []\n for (const [key, doc] of items) fields.push(key, JSON.stringify({ d: doc, e: null }))\n await this.#client\n .multi()\n .hset(this.#docs(collection), ...fields)\n .zrem(this.#expiry(collection), ...items.keys())\n .exec()\n }\n\n /** Removes every document whose TTL has passed. Reads never return them in the meantime. */\n async sweep(): Promise<void> {\n try {\n const collections = await this.#client.smembers(this.#expiring())\n for (const collection of collections) {\n const now = this.#now()\n let removed: number\n // A full batch may have left more behind.\n do {\n removed = Number(\n await this.#client.nymbalSweep(\n this.#docs(collection),\n this.#expiry(collection),\n now,\n SWEEP_BATCH,\n ),\n )\n } while (removed === SWEEP_BATCH)\n }\n } catch (err) {\n this.#logger?.warn({ err }, 'redis document store: expired documents were not swept')\n }\n }\n\n async close(): Promise<void> {\n if (this.#sweepTimer !== null) clearInterval(this.#sweepTimer)\n this.#sweepTimer = null\n await this.#client.quit().catch(() => this.#client.disconnect())\n }\n\n #docs(collection: string): string {\n return `${this.#prefix}:doc:${collection}`\n }\n\n #expiry(collection: string): string {\n return `${this.#prefix}:exp:${collection}`\n }\n\n #expiring(): string {\n return `${this.#prefix}:expiring`\n }\n\n #decode(raw: string | null): Stored | null {\n if (raw === null) return null\n const stored = JSON.parse(raw) as Stored\n if (stored.e !== null && stored.e <= this.#now()) return null\n return stored\n }\n\n #live<T>(raw: string | null): T | null {\n const stored = this.#decode(raw)\n return stored === null ? null : (stored.d as T)\n }\n}\n\n/**\n * PING on a client of its own that never retries and queues nothing, disconnected afterwards: the store's or bus's own\n * clients retry a closed port for as long as the process lives, so they cannot answer within the bound.\n */\nfunction pingRedis(url: string, timeoutMs = HEALTH_CHECK_TIMEOUT_MS): Promise<HealthReport> {\n const client = new Redis(url, {\n lazyConnect: true,\n retryStrategy: () => null,\n maxRetriesPerRequest: 0,\n enableOfflineQueue: false,\n connectTimeout: timeoutMs,\n })\n // connect() rejects with the same error, and the report carries it; without a listener ioredis would throw it.\n client.on('error', () => undefined)\n return boundedHealthCheck(\n async () => {\n await client.connect()\n await client.ping()\n return { status: 'healthy' }\n },\n { timeoutMs, redact: connectionSecrets(url) },\n ).finally(() => client.disconnect())\n}\n\n/** Connects to `url` and resolves once the connection is ready; a server that cannot be reached rejects. */\nexport async function connectRedis(url: string, logger?: Logger): Promise<Redis> {\n const client = new Redis(url, { lazyConnect: true })\n client.on('error', (err: Error) => logger?.warn({ err }, 'redis connection error'))\n try {\n await client.connect()\n } catch (error) {\n client.disconnect()\n throw error\n }\n return client\n}\n\nexport async function createRedisDocumentStore(\n options: RedisDocumentStoreOptions,\n): Promise<RedisDocumentStore> {\n const client = await connectRedis(options.url, options.logger)\n return new RedisDocumentStore(client, options)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/redis-document-store.ts"],"sourcesContent":["import { ConfigError, type AdapterContext, type DocumentStoreAdapter } from '@nymbalhq/types'\nimport {\n createRedisDocumentStore,\n RedisDocumentStore,\n type RedisDocumentStoreOptions,\n} from './redis-document-store.js'\n\nexport { createRedisDocumentStore, RedisDocumentStore, type RedisDocumentStoreOptions }\n\n/** The kernel's entry: `infrastructure.documentStore: 'redis'`, with `{ url, prefix }` from the `redis` block. */\nexport async function createAdapter(ctx: AdapterContext): Promise<DocumentStoreAdapter> {\n if (ctx.slot !== 'documentStore') {\n throw new ConfigError(`@nymbalhq/doc-store-redis serves documentStore, not ${ctx.slot}`, {\n code: 'adapter.wrong_slot',\n context: { slot: ctx.slot },\n })\n }\n const { url, prefix } = ctx.config\n if (typeof url !== 'string' || url === '' || typeof prefix !== 'string' || prefix === '') {\n throw new ConfigError('@nymbalhq/doc-store-redis needs a Redis url and a key prefix', {\n code: 'config.invalid',\n context: { slot: ctx.slot },\n })\n }\n return createRedisDocumentStore({ url, prefix, logger: ctx.logger })\n}\n","import { randomUUID } from 'node:crypto'\nimport { Redis } from 'ioredis'\nimport {\n AdapterError,\n boundedHealthCheck,\n connectionSecrets,\n HEALTH_CHECK_TIMEOUT_MS,\n queryDocuments,\n type DocumentStoreAdapter,\n type HealthReport,\n type Logger,\n type PutOptions,\n type QueryParams,\n type QueryResult,\n} from '@nymbalhq/types'\n\nexport interface RedisDocumentStoreOptions {\n /** `redis://[user:password@]host:port[/db]`; the db index in the path is honoured. */\n url: string\n /** Namespaces every key the store writes: `<prefix>:doc:<collection>` and its expiry index. */\n prefix: string\n logger?: Logger\n /** How often expired documents are removed (they are never returned once expired). 0 turns the sweep off. */\n sweepIntervalMs?: number\n now?: () => number\n}\n\n/** A stored document and when it expires (epoch ms), or null for never. */\ninterface Stored {\n d: unknown\n e: number | null\n}\n\n// Writes a document and keeps the collection's expiry index in step with it.\n// KEYS: documents hash, expiry zset, expiring-collections set. ARGV: key, stored JSON, expiresAt or '', collection.\nconst PUT = `\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\nif ARGV[3] == '' then\n redis.call('ZREM', KEYS[2], ARGV[1])\nelse\n redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])\n redis.call('SADD', KEYS[3], ARGV[4])\nend\nreturn 1\n`\n\n// The same write, only when the live document's field is strictly equal to the expected value (JavaScript `===`:\n// objects and arrays never are). ARGV[5]: the field; ARGV[6]: the expected value as JSON, or '' for undefined;\n// ARGV[7]: now (epoch ms).\nconst PUT_IF = `\nlocal current = redis.call('HGET', KEYS[1], ARGV[1])\nif not current then return 'absent' end\nlocal stored = cjson.decode(current)\nif stored.e ~= cjson.null and tonumber(stored.e) <= tonumber(ARGV[7]) then return 'absent' end\nlocal actual = nil\nif type(stored.d) == 'table' then actual = stored.d[ARGV[5]] end\nif ARGV[6] == '' then\n if actual ~= nil then return 'mismatch' end\nelse\n local expected = cjson.decode(ARGV[6])\n if type(expected) == 'table' or type(actual) == 'table' then return 'mismatch' end\n if actual ~= expected then return 'mismatch' end\nend\nredis.call('HSET', KEYS[1], ARGV[1], ARGV[2])\nif ARGV[3] == '' then\n redis.call('ZREM', KEYS[2], ARGV[1])\nelse\n redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])\n redis.call('SADD', KEYS[3], ARGV[4])\nend\nreturn 'ok'\n`\n\n// Removes up to ARGV[2] documents whose expiry has passed at ARGV[1], checking each again under the script, so a\n// document written again since it was indexed is kept.\nconst SWEEP = `\nlocal due = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', ARGV[1], 'LIMIT', 0, tonumber(ARGV[2]))\nfor _, key in ipairs(due) do\n local current = redis.call('HGET', KEYS[1], key)\n if current then\n local stored = cjson.decode(current)\n if stored.e ~= cjson.null and tonumber(stored.e) <= tonumber(ARGV[1]) then\n redis.call('HDEL', KEYS[1], key)\n end\n end\n redis.call('ZREM', KEYS[2], key)\nend\nreturn #due\n`\n\ntype Script = (...args: Array<string | number>) => Promise<unknown>\ninterface Scripted {\n nymbalPut: Script\n nymbalPutIf: Script\n nymbalSweep: Script\n}\n\nconst SWEEP_BATCH = 500\n/** Documents per HSET while a replace stages its new contents. */\nconst REPLACE_BATCH = 500\n\n/** A document store on Redis: one hash per collection under the prefix, ordered and filtered by the reference. */\nexport class RedisDocumentStore implements DocumentStoreAdapter {\n readonly #client: Redis & Scripted\n readonly #url: string\n readonly #prefix: string\n readonly #now: () => number\n readonly #logger: Logger | undefined\n #sweepTimer: NodeJS.Timeout | null = null\n\n constructor(client: Redis, options: RedisDocumentStoreOptions) {\n client.defineCommand('nymbalPut', { numberOfKeys: 3, lua: PUT })\n client.defineCommand('nymbalPutIf', { numberOfKeys: 3, lua: PUT_IF })\n client.defineCommand('nymbalSweep', { numberOfKeys: 2, lua: SWEEP })\n this.#client = client as Redis & Scripted\n this.#url = options.url\n this.#prefix = options.prefix\n this.#now = options.now ?? Date.now\n this.#logger = options.logger\n const interval = options.sweepIntervalMs ?? 60_000\n if (interval > 0) {\n this.#sweepTimer = setInterval(() => void this.sweep(), interval)\n this.#sweepTimer.unref()\n }\n }\n\n capabilities(): readonly string[] {\n return ['get', 'put', 'delete', 'query', 'batch', 'ttl', 'conditional-put']\n }\n\n healthCheck(): Promise<HealthReport> {\n return pingRedis(this.#url)\n }\n\n async get<T>(collection: string, key: string): Promise<T | null> {\n const raw = await this.#client.hget(this.#docs(collection), key)\n return this.#live<T>(raw)\n }\n\n async put<T>(\n collection: string,\n key: string,\n document: T,\n options: PutOptions = {},\n ): Promise<void> {\n const expiresAt = options.ttl !== undefined ? this.#now() + options.ttl * 1000 : null\n const stored = JSON.stringify({ d: document, e: expiresAt } satisfies Stored)\n const keys = [this.#docs(collection), this.#expiry(collection), this.#expiring()]\n const args = [key, stored, expiresAt === null ? '' : String(expiresAt), collection]\n if (options.conditionKey === undefined) {\n await this.#client.nymbalPut(...keys, ...args)\n return\n }\n const expected =\n options.conditionValue === undefined ? '' : JSON.stringify(options.conditionValue)\n const outcome = await this.#client.nymbalPutIf(\n ...keys,\n ...args,\n options.conditionKey,\n expected,\n this.#now(),\n )\n if (outcome !== 'ok') {\n throw new AdapterError(\n 'condition-failed',\n outcome === 'absent'\n ? `Condition failed on ${collection}/${key}: document does not exist`\n : `Condition failed on ${collection}/${key}: ${options.conditionKey} mismatch`,\n { context: { collection, key, conditionKey: options.conditionKey } },\n )\n }\n }\n\n async delete(collection: string, key: string): Promise<void> {\n await this.#client\n .multi()\n .hdel(this.#docs(collection), key)\n .zrem(this.#expiry(collection), key)\n .exec()\n }\n\n async query<T>(collection: string, params: QueryParams): Promise<QueryResult<T>> {\n const all = await this.#client.hgetall(this.#docs(collection))\n const live: Array<[string, unknown]> = []\n for (const [key, raw] of Object.entries(all)) {\n const stored = this.#decode(raw)\n if (stored !== null) live.push([key, stored.d])\n }\n return queryDocuments<T>(live, params)\n }\n\n async batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>> {\n const result = new Map<string, T>()\n if (keys.length === 0) return result\n const values = await this.#client.hmget(this.#docs(collection), ...keys)\n keys.forEach((key, i) => {\n const doc = this.#live<T>(values[i] ?? null)\n if (doc !== null) result.set(key, doc)\n })\n return result\n }\n\n async batchPut<T>(collection: string, items: Map<string, T>): Promise<void> {\n if (items.size === 0) return\n const fields: string[] = []\n for (const [key, doc] of items) fields.push(key, JSON.stringify({ d: doc, e: null }))\n await this.#client\n .multi()\n .hset(this.#docs(collection), ...fields)\n .zrem(this.#expiry(collection), ...items.keys())\n .exec()\n }\n\n /**\n * Writes the new contents to a staging hash under the prefix, then swaps it in with RENAME in one MULTI, dropping the\n * collection's expiry index with it (the new documents have no TTL). A source that throws deletes the staging hash.\n */\n async replaceCollection<T>(\n collection: string,\n entries: AsyncIterable<[string, T]>,\n ): Promise<void> {\n const staging = `${this.#prefix}:stage:${randomUUID()}:doc:${collection}`\n let staged = 0\n try {\n let fields: string[] = []\n const flush = async () => {\n if (fields.length === 0) return\n await this.#client.hset(staging, ...fields)\n fields = []\n }\n for await (const [key, doc] of entries) {\n fields.push(key, JSON.stringify({ d: doc, e: null } satisfies Stored))\n staged += 1\n if (fields.length >= REPLACE_BATCH * 2) await flush()\n }\n await flush()\n } catch (error) {\n await this.#client\n .del(staging)\n .catch((err: unknown) =>\n this.#logger?.warn({ err, staging }, 'redis document store: a staging hash was left'),\n )\n throw error\n }\n const swap = this.#client.multi()\n if (staged > 0) swap.rename(staging, this.#docs(collection))\n else swap.del(this.#docs(collection))\n const results = await swap.del(this.#expiry(collection)).exec()\n const failed = results?.find(([err]) => err !== null)?.[0]\n if (failed) throw failed\n }\n\n /** Removes every document whose TTL has passed. Reads never return them in the meantime. */\n async sweep(): Promise<void> {\n try {\n const collections = await this.#client.smembers(this.#expiring())\n for (const collection of collections) {\n const now = this.#now()\n let removed: number\n // A full batch may have left more behind.\n do {\n removed = Number(\n await this.#client.nymbalSweep(\n this.#docs(collection),\n this.#expiry(collection),\n now,\n SWEEP_BATCH,\n ),\n )\n } while (removed === SWEEP_BATCH)\n }\n } catch (err) {\n this.#logger?.warn({ err }, 'redis document store: expired documents were not swept')\n }\n }\n\n async close(): Promise<void> {\n if (this.#sweepTimer !== null) clearInterval(this.#sweepTimer)\n this.#sweepTimer = null\n await this.#client.quit().catch(() => this.#client.disconnect())\n }\n\n #docs(collection: string): string {\n return `${this.#prefix}:doc:${collection}`\n }\n\n #expiry(collection: string): string {\n return `${this.#prefix}:exp:${collection}`\n }\n\n #expiring(): string {\n return `${this.#prefix}:expiring`\n }\n\n #decode(raw: string | null): Stored | null {\n if (raw === null) return null\n const stored = JSON.parse(raw) as Stored\n if (stored.e !== null && stored.e <= this.#now()) return null\n return stored\n }\n\n #live<T>(raw: string | null): T | null {\n const stored = this.#decode(raw)\n return stored === null ? null : (stored.d as T)\n }\n}\n\n/**\n * PING on a client of its own that never retries and queues nothing, disconnected afterwards: the store's or bus's own\n * clients retry a closed port for as long as the process lives, so they cannot answer within the bound.\n */\nfunction pingRedis(url: string, timeoutMs = HEALTH_CHECK_TIMEOUT_MS): Promise<HealthReport> {\n const client = new Redis(url, {\n lazyConnect: true,\n retryStrategy: () => null,\n maxRetriesPerRequest: 0,\n enableOfflineQueue: false,\n connectTimeout: timeoutMs,\n })\n // connect() rejects with the same error, and the report carries it; without a listener ioredis would throw it.\n client.on('error', () => undefined)\n return boundedHealthCheck(\n async () => {\n await client.connect()\n await client.ping()\n return { status: 'healthy' }\n },\n { timeoutMs, redact: connectionSecrets(url) },\n ).finally(() => client.disconnect())\n}\n\n/** Connects to `url` and resolves once the connection is ready; a server that cannot be reached rejects. */\nexport async function connectRedis(url: string, logger?: Logger): Promise<Redis> {\n // While the server is gone a command waits out at most two reconnect attempts (each a connect of at most 2 s, a\n // second apart) and then rejects, rather than queueing until the server returns. The client keeps reconnecting for\n // as long as the store is open, so the commands after the server is back succeed.\n const client = new Redis(url, {\n lazyConnect: true,\n maxRetriesPerRequest: 1,\n retryStrategy: (times: number) => Math.min(times * 200, 1000),\n connectTimeout: 2000,\n })\n client.on('error', (err: Error) => logger?.warn({ err }, 'redis connection error'))\n try {\n await client.connect()\n } catch (error) {\n client.disconnect()\n throw error\n }\n return client\n}\n\nexport async function createRedisDocumentStore(\n options: RedisDocumentStoreOptions,\n): Promise<RedisDocumentStore> {\n const client = await connectRedis(options.url, options.logger)\n return new RedisDocumentStore(client, options)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nymbalhq/doc-store-redis",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "The Redis document store for Nymbal: `infrastructure.documentStore: 'redis'`",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,22 +29,23 @@
29
29
  "recipes"
30
30
  ],
31
31
  "dependencies": {
32
- "@nymbalhq/types": "1.1.0"
32
+ "@nymbalhq/types": "1.3.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "ioredis": "^5.4.0 || ^6.0.0"
36
36
  },
37
37
  "devDependencies": {
38
+ "@nymbalhq/contract-tests": "1.3.0",
39
+ "@typescript/native": "npm:typescript@^7.0.2",
38
40
  "fast-check": "^4.10.2",
39
41
  "ioredis": "^6.0.0",
40
- "tsup": "^8.3.5",
41
- "typescript": "^5.6.3",
42
- "vitest": "^3.0.0",
43
- "@nymbalhq/contract-tests": "0.1.0"
42
+ "tsup": "^9.0.0",
43
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
44
+ "vitest": "^3.0.0"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "tsup",
47
48
  "dev": "tsup --watch",
48
- "typecheck": "tsc -p tsconfig.json --noEmit"
49
+ "typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../.."
49
50
  }
50
51
  }