valkyries 0.1.0 → 0.1.2

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/docs/cache.md CHANGED
@@ -17,3 +17,194 @@ Values larger than 2 KiB are gzip-compressed. Null results are cached with `null
17
17
  `cacheGetByAny()` and `cacheGetByAnyBatch()` protect against stale miss writes by setting short-lived invalidation markers during delete operations.
18
18
 
19
19
  Optional Bloom filter integration can skip authoritative-store calls for definite misses. If the Bloom filter is unavailable or not ready, cache reads fall back to the normal miss path.
20
+
21
+ ## Import
22
+
23
+ ```ts
24
+ import { ValkeyCache } from "valkyries";
25
+ ```
26
+
27
+ or:
28
+
29
+ ```ts
30
+ import { ValkeyCache } from "valkyries/cache";
31
+ ```
32
+
33
+ ## Constructor
34
+
35
+ ```ts
36
+ const cache = new ValkeyCache({ prefix: "users", ttlSeconds: 300 });
37
+ ```
38
+
39
+ ```ts
40
+ type ValkeyCacheOptions<K = string> = {
41
+ prefix: string;
42
+ ttlSeconds: number;
43
+ nullTtlSeconds?: number;
44
+ mode?: "json" | "text" | "buffer";
45
+ staleTtlAge?: number;
46
+ bloomFilter?: ValkeyBloomFilter;
47
+ bloomFilterEnabled?: () => boolean;
48
+ client?: GlideClient;
49
+ keySerializer?: (key: K) => string;
50
+ };
51
+ ```
52
+
53
+ For non-string keys, `keySerializer` is required.
54
+
55
+ - `prefix`: namespace for this cache.
56
+ - `ttlSeconds`: TTL for non-null values.
57
+ - `nullTtlSeconds`: TTL for cached nulls. Defaults to `Math.max(1, Math.floor(ttlSeconds / 60))`.
58
+ - `mode`: serialization mode. Defaults to `json`.
59
+ - `staleTtlAge`: refresh-ahead threshold from `0` to `1`. Defaults to `0.9`.
60
+ - `bloomFilter`: optional negative-lookup Bloom filter.
61
+ - `bloomFilterEnabled`: optional per-call gate for Bloom filter checks.
62
+ - `client`: optional `@valkey/valkey-glide` client. Defaults to the package cache client.
63
+ - `keySerializer`: converts keys to strings before trimming and lowercasing.
64
+
65
+ The constructor throws when `prefix` is empty, `ttlSeconds` is not positive, or `staleTtlAge` is outside `0..1`.
66
+
67
+ ## `cacheGetByAny(fn)`
68
+
69
+ ```ts
70
+ cache.cacheGetByAny<T>(
71
+ fn: (key: K) => Promise<T | null | undefined>,
72
+ ): (key: K) => Promise<T | null>
73
+ ```
74
+
75
+ Returns a read-through single-key cache wrapper.
76
+
77
+ Behavior:
78
+
79
+ - Invalid, null, undefined, or empty serialized keys return `null`.
80
+ - Cache hits return the decoded value.
81
+ - Cached nulls return `null` without calling `fn`.
82
+ - Misses call `fn(key)`.
83
+ - `undefined` from `fn` is normalized to `null`.
84
+ - Miss results are written asynchronously when no invalidation marker is present.
85
+ - Stale hits can trigger asynchronous refresh-ahead.
86
+
87
+ ## `cacheGetByAnyBatch(batchFn)`
88
+
89
+ ```ts
90
+ cache.cacheGetByAnyBatch<T>(
91
+ batchFn: (keys: K[]) => Promise<Array<T | null | undefined>>,
92
+ ): (keys: K[]) => Promise<Array<T | null>>
93
+ ```
94
+
95
+ Returns a read-through batch cache wrapper.
96
+
97
+ Behavior:
98
+
99
+ - Input order and length are preserved.
100
+ - Invalid keys return `null` in their original output positions.
101
+ - Duplicate serialized keys are fetched once and scattered back to every duplicate position.
102
+ - `batchFn` is called only for real misses.
103
+ - `batchFn` must return an array with exactly the same length as its input.
104
+ - `undefined` values are normalized to `null`.
105
+ - Fetched values are written asynchronously when no invalidation marker is present.
106
+ - Stale hits can trigger asynchronous batch refresh-ahead.
107
+
108
+ ## `get(key)`
109
+
110
+ ```ts
111
+ cache.get(key: K): Promise<string | Buffer | Record<string, unknown> | null>
112
+ ```
113
+
114
+ Reads a cache entry directly without calling a fallback function. Returns `null` for misses, invalid keys, cached nulls, and Bloom-filter negative lookups.
115
+
116
+ ## `getBatch(keys)`
117
+
118
+ ```ts
119
+ cache.getBatch(keys: K[]): Promise<Array<string | Buffer | Record<string, unknown> | null>>
120
+ ```
121
+
122
+ Reads multiple cache entries directly. Output order and length match the input.
123
+
124
+ ## `set(key, value, ttl?)`
125
+
126
+ ```ts
127
+ cache.set(key: K, value: unknown, ttl?: number): Promise<void>
128
+ ```
129
+
130
+ Writes a value directly.
131
+
132
+ - Positive `ttl` overrides the cache TTL for this entry.
133
+ - `null` and `undefined` values use `nullTtlSeconds`.
134
+ - Invalid keys are ignored.
135
+ - In `text` mode, `value` must be a string.
136
+ - In `buffer` mode, `value` must be a `Buffer`.
137
+
138
+ ## `setBatch(entries)`
139
+
140
+ ```ts
141
+ cache.setBatch(
142
+ entries: Array<{ key: K; value: unknown; ttl?: number }>,
143
+ ): Promise<void>
144
+ ```
145
+
146
+ Writes multiple values directly using a Valkey batch.
147
+
148
+ ## `refreshById(aliases, fetchByKey)`
149
+
150
+ ```ts
151
+ cache.refreshById<T>(
152
+ aliases: K[],
153
+ fetchByKey: (key: K) => Promise<T | null | undefined>,
154
+ ): Promise<T | null>
155
+ ```
156
+
157
+ Fetches fresh data once using the first valid alias, then writes the result to every valid alias. Use this when one entity can be addressed by several cache keys.
158
+
159
+ ## `delete(...keys)`
160
+
161
+ ```ts
162
+ cache.delete(...keys: K[]): Promise<number>
163
+ ```
164
+
165
+ Deletes cache entries and writes short-lived invalidation markers so in-flight read-through writes do not repopulate stale data.
166
+
167
+ ## `invalidateCacheGetByAny(...keys)`
168
+
169
+ ```ts
170
+ cache.invalidateCacheGetByAny(...keys: K[]): Promise<number>
171
+ ```
172
+
173
+ Alias for `delete(...keys)`.
174
+
175
+ ## `invalidate()`
176
+
177
+ ```ts
178
+ cache.invalidate(): Promise<void>
179
+ ```
180
+
181
+ Deletes every cache key for this cache prefix.
182
+
183
+ ## `ValkeyCache.invalidate(prefix, client?)`
184
+
185
+ ```ts
186
+ ValkeyCache.invalidate(prefix: string, client?: GlideClient): Promise<void>
187
+ ```
188
+
189
+ Deletes every cache key for `prefix`. Passing an empty prefix invalidates all cache namespaces visible to the client.
190
+
191
+ ## `getKey(key)`
192
+
193
+ ```ts
194
+ cache.getKey(key: K): string
195
+ ```
196
+
197
+ Returns the Valkey cache key for a logical key. Throws when the logical key serializes to an invalid cache key.
198
+
199
+ ## Events
200
+
201
+ The cache emits:
202
+
203
+ - `cache:call` with cache name, batch flag, hit/miss/bloom-miss counts, and duration.
204
+ - `cache:hit` with `{ cacheName, keys, count }`.
205
+ - `cache:miss` with `{ cacheName, keys, count }`.
206
+ - `cache:bloom-miss` with `{ cacheName, keys, count }`.
207
+ - `cache:set` with `{ cacheName, keys }`.
208
+ - `cache:set-skipped` with `{ cacheName, keys }`.
209
+ - `cache:delete` with `{ cacheName, keys }`.
210
+ - `cache:invalidate` with `{ cacheName }`.
package/docs/clients.md CHANGED
@@ -11,3 +11,101 @@ Connections are deduplicated by URL, read preference, and lazy-connect setting t
11
11
  For libraries, tests, and multi-tenant services, pass a `GlideClient` directly to `ValkeyCache`, `ValkeyBloomFilter`, `DynamicConfig`, or `RateLimiter`.
12
12
 
13
13
  Use `closeValkeyClients()` or `onGracefulShutdown()` to close package-managed clients and the dynamic-config pub/sub connection.
14
+
15
+ ## Default Clients
16
+
17
+ ```ts
18
+ import { cacheValkeyClient, rateLimiterValkeyClient, dynamicConfigValkeyClient } from "valkyries";
19
+ ```
20
+
21
+ Default clients are created at module load:
22
+
23
+ - `cacheValkeyClient`: uses `VALKEY_CACHE_URL` or `VALKEY_URL`, reads from replicas when possible.
24
+ - `rateLimiterValkeyClient`: uses `VALKEY_RATE_LIMITER_URL`, `VALKEY_CACHE_URL`, or `VALKEY_URL`, and reads from primary.
25
+ - `dynamicConfigValkeyClient`: uses `VALKEY_DYNAMIC_CONFIG_URL` or `VALKEY_URL`, reads from replicas when possible.
26
+
27
+ ## `ValkeyClientOptions`
28
+
29
+ ```ts
30
+ type ValkeyClientOptions = {
31
+ readFrom?: "primary" | "preferReplica";
32
+ lazyConnect?: boolean;
33
+ };
34
+ ```
35
+
36
+ - `readFrom`: Glide read preference.
37
+ - `lazyConnect`: whether Glide should connect lazily. Defaults to `true`.
38
+
39
+ ## `urlsToClients`
40
+
41
+ ```ts
42
+ const urlsToClients: Map<string, GlideClient>;
43
+ ```
44
+
45
+ Registry of package-managed clients keyed by URL, read preference, and lazy-connect setting.
46
+
47
+ ## `upsertValkeyClientByUrl(url, options?)`
48
+
49
+ ```ts
50
+ upsertValkeyClientByUrl(
51
+ url: string,
52
+ options?: ValkeyClientOptions,
53
+ ): Promise<GlideClient>
54
+ ```
55
+
56
+ Returns an existing matching client or creates and stores a new one.
57
+
58
+ ## `glideConfigFromUrl(url, options?)`
59
+
60
+ ```ts
61
+ glideConfigFromUrl(url: string, options?: ValkeyClientOptions)
62
+ ```
63
+
64
+ Builds a Glide client configuration from `redis://` or `rediss://` URLs.
65
+
66
+ Supported URL fields:
67
+
68
+ - host
69
+ - port
70
+ - username
71
+ - password
72
+ - TLS through `rediss://`
73
+
74
+ Throws `Invalid Valkey URL` for malformed URLs.
75
+
76
+ ## Dynamic Config Pub/Sub Client
77
+
78
+ ```ts
79
+ ensureDynamicConfigValkeySubscriptionClient(): Promise<GlideClient>
80
+ closeDynamicConfigValkeySubscriptionClient(): Promise<void>
81
+ ```
82
+
83
+ `ensureDynamicConfigValkeySubscriptionClient()` creates or returns the shared pub/sub client subscribed to `dynamic-config:*`.
84
+
85
+ `closeDynamicConfigValkeySubscriptionClient()` unsubscribes and closes the shared pub/sub client.
86
+
87
+ ## Pub/Sub Handlers
88
+
89
+ ```ts
90
+ addPubSubMessageHandler(handler: (msg: PubSubMsg) => void): void
91
+ removePubSubMessageHandler(handler: (msg: PubSubMsg) => void): void
92
+ ```
93
+
94
+ Registers or removes handlers that receive dynamic-config pub/sub messages.
95
+
96
+ ## Shutdown
97
+
98
+ ```ts
99
+ closeValkeyClients(): Promise<void>
100
+ onGracefulShutdown(): Promise<void>
101
+ ```
102
+
103
+ Both exports point to the same idempotent shutdown function. It closes all live dynamic configs, package-managed clients, and the dynamic-config subscription client.
104
+
105
+ ## `deleteKeysWithPrefix(client, pattern)`
106
+
107
+ ```ts
108
+ deleteKeysWithPrefix(client: GlideClient, pattern: string): Promise<void>
109
+ ```
110
+
111
+ Scans keys matching `pattern` and unlinks them in batches. This helper is used by cache and rate-limiter invalidation APIs.
@@ -11,3 +11,135 @@ Fields are declared with `fieldTypes` and initialized from `defaultFields`. Miss
11
11
  Use `setField()` or `setFields()` to update values. Updates are written with a Lua script that performs `HSET` and `PUBLISH` in one roundtrip.
12
12
 
13
13
  Each instance subscribes to `dynamic-config:*` and applies matching field updates from pub/sub. Call `close()` in tests and shutdown paths to remove handlers and clear refresh timers.
14
+
15
+ ## Import
16
+
17
+ ```ts
18
+ import { DynamicConfig } from "valkyries";
19
+ ```
20
+
21
+ or:
22
+
23
+ ```ts
24
+ import { DynamicConfig } from "valkyries/dynamic-config";
25
+ ```
26
+
27
+ ## Constructor
28
+
29
+ ```ts
30
+ const flags = new DynamicConfig({
31
+ key: "feature-flags",
32
+ fieldTypes: { enabled: "boolean", sampleRate: "number" },
33
+ defaultFields: { enabled: false, sampleRate: 0 },
34
+ });
35
+ ```
36
+
37
+ ```ts
38
+ type DynamicConfigOptions = {
39
+ staleTtlSeconds?: number | null;
40
+ key: string;
41
+ fieldTypes: Record<string, "string" | "number" | "boolean">;
42
+ defaultFields: Record<string, string | number | boolean>;
43
+ client?: GlideClient;
44
+ };
45
+ ```
46
+
47
+ - `staleTtlSeconds`: refresh interval for local fields. Defaults to `60`.
48
+ - `key`: logical config name. The hash key is `dynamic-config:${key}`.
49
+ - `fieldTypes`: allowed fields and primitive types.
50
+ - `defaultFields`: default values written when fields are missing.
51
+ - `client`: optional `@valkey/valkey-glide` client. Defaults to the package dynamic-config client.
52
+
53
+ The constructor starts initialization immediately. Call `waitForInitialization()` before reading fields during startup.
54
+
55
+ Outside `NODE_ENV=test`, constructing multiple `DynamicConfig` instances with the same key throws.
56
+
57
+ ## Public Properties
58
+
59
+ - `staleTtl`: refresh interval in seconds.
60
+ - `key`: Valkey hash key.
61
+ - `fields`: local field map.
62
+ - `fieldTypes`: field schema.
63
+ - `defaultFields`: default values.
64
+ - `initialization`: initialization promise.
65
+
66
+ ## `waitForInitialization()`
67
+
68
+ ```ts
69
+ config.waitForInitialization(): Promise<void>
70
+ ```
71
+
72
+ Waits for field validation, default writes, initial field loading, pub/sub subscription, and timer creation.
73
+
74
+ ## `getFields()`
75
+
76
+ ```ts
77
+ config.getFields(): Record<string, string | number | boolean>
78
+ ```
79
+
80
+ Returns a plain object snapshot of local fields.
81
+
82
+ ## `setField(name, value)`
83
+
84
+ ```ts
85
+ config.setField(name: string, value: string | number | boolean): Promise<void>
86
+ ```
87
+
88
+ Writes one field and publishes the update atomically. Throws for unknown fields or invalid values for the field type.
89
+
90
+ ## `setFields(fields)`
91
+
92
+ ```ts
93
+ config.setFields(fields: Record<string, string | number | boolean>): Promise<void>
94
+ ```
95
+
96
+ Writes multiple fields and publishes updates atomically. Local state is updated only after the Valkey write succeeds.
97
+
98
+ ## `refresh()`
99
+
100
+ ```ts
101
+ config.refresh(): Promise<void>
102
+ ```
103
+
104
+ Refreshes local fields from Valkey when the stale TTL has elapsed. Concurrent refreshes are coalesced by an optimistic timestamp update.
105
+
106
+ ## `subscribe()`
107
+
108
+ ```ts
109
+ config.subscribe(): Promise<void>
110
+ ```
111
+
112
+ Ensures the shared dynamic-config subscription client exists and registers this config's message handler.
113
+
114
+ ## `unsubscribe()`
115
+
116
+ ```ts
117
+ config.unsubscribe(): void
118
+ ```
119
+
120
+ Removes this config's message handler from the shared pub/sub dispatcher.
121
+
122
+ ## `close()`
123
+
124
+ ```ts
125
+ config.close(): Promise<void>
126
+ ```
127
+
128
+ Marks the config closed, unsubscribes, clears the refresh timer, and removes it from the global dynamic config registry.
129
+
130
+ ## `stringifyField` and `parseField`
131
+
132
+ ```ts
133
+ config.stringifyField(type, value): string;
134
+ config.parseField(type, value): string | number | boolean;
135
+ ```
136
+
137
+ Helpers used internally for Valkey storage and pub/sub messages.
138
+
139
+ ## `dynamicConfigs`
140
+
141
+ ```ts
142
+ import { dynamicConfigs } from "valkyries";
143
+ ```
144
+
145
+ Array of live `DynamicConfig` instances. It is used by `closeValkeyClients()` to close configs during shutdown.
@@ -13,3 +13,102 @@ setValkeyErrorHandler((error) => {
13
13
  logger.error(error);
14
14
  });
15
15
  ```
16
+
17
+ ## `valkeyEvents`
18
+
19
+ ```ts
20
+ import { valkeyEvents } from "valkyries";
21
+ ```
22
+
23
+ `valkeyEvents` is a typed `EventEmitter`.
24
+
25
+ Example:
26
+
27
+ ```ts
28
+ valkeyEvents.on("cache:call", (event) => {
29
+ metrics.histogram("cache.call.duration", event.durationMs, {
30
+ cache: event.cacheName,
31
+ batch: String(event.batch),
32
+ });
33
+ });
34
+ ```
35
+
36
+ ## `emitValkeyEvent(event, payload)`
37
+
38
+ ```ts
39
+ emitValkeyEvent<K extends keyof ValkeyEventMap>(
40
+ event: K,
41
+ ...args: ValkeyEventMap[K]
42
+ ): void
43
+ ```
44
+
45
+ Emits an event and routes listener errors to the configured Valkey error handler.
46
+
47
+ ## Event Payloads
48
+
49
+ Cache events:
50
+
51
+ ```ts
52
+ "cache:call": [{
53
+ cacheName: string;
54
+ batch: boolean;
55
+ hits: number;
56
+ misses: number;
57
+ bloomMisses: number;
58
+ durationMs: number;
59
+ }];
60
+ "cache:hit": [{ cacheName: string; keys: string[]; count: number }];
61
+ "cache:miss": [{ cacheName: string; keys: string[]; count: number }];
62
+ "cache:bloom-miss": [{ cacheName: string; keys: string[]; count: number }];
63
+ "cache:set": [{ cacheName: string; keys: string[] }];
64
+ "cache:set-skipped": [{ cacheName: string; keys: string[] }];
65
+ "cache:delete": [{ cacheName: string; keys: string[] }];
66
+ "cache:invalidate": [{ cacheName: string }];
67
+ ```
68
+
69
+ Bloom filter events:
70
+
71
+ ```ts
72
+ "bloom-filter:exists": [{ name: string; item: string; result: boolean | null }];
73
+ "bloom-filter:mexists": [{
74
+ name: string;
75
+ items: string[];
76
+ results: Array<boolean | null>;
77
+ }];
78
+ "bloom-filter:add": [{ name: string; items: string[] }];
79
+ ```
80
+
81
+ Rate limiter events:
82
+
83
+ ```ts
84
+ "rate-limiter:add": [{ prefix: string; ids: string[] }];
85
+ "rate-limiter:get": [{ prefix: string; ids: string[]; counts: number[] }];
86
+ "rate-limiter:delete": [{ prefix: string; ids: string[] }];
87
+ "rate-limiter:invalidate": [{ prefix: string }];
88
+ ```
89
+
90
+ ## `trackCacheCall(metric)`
91
+
92
+ ```ts
93
+ trackCacheCall(metric: {
94
+ cacheName: string;
95
+ batch: boolean;
96
+ hits: number;
97
+ misses: number;
98
+ bloomMisses: number;
99
+ duration: number;
100
+ }): void
101
+ ```
102
+
103
+ Emits `cache:call` with `duration` renamed to `durationMs`.
104
+
105
+ ## Error Handling
106
+
107
+ ```ts
108
+ setValkeyErrorHandler(handler: (error: Error) => void): void
109
+ handleValkeyError(error: unknown): void
110
+ ```
111
+
112
+ `setValkeyErrorHandler()` installs the process-wide handler used for asynchronous write failures, event listener failures, background refresh failures, and other non-fatal Valkey errors.
113
+
114
+ `handleValkeyError()` normalizes any thrown value to an `Error` and passes it to the configured handler.
@@ -5,3 +5,48 @@ Lua scripts live in `scripts/` and are copied to `dist/scripts/` during build.
5
5
  Use `loadScript()` and `registerScript()` for package scripts. Registered scripts are released on process exit.
6
6
 
7
7
  Scripts are used when operations must be atomic or when combining multiple Valkey commands into one roundtrip.
8
+
9
+ ## `loadScript(relativePath, baseUrl)`
10
+
11
+ ```ts
12
+ loadScript(relativePath: string, baseUrl: string | URL): string
13
+ ```
14
+
15
+ Loads a Lua script from `scripts/` relative to `baseUrl`.
16
+
17
+ ## `registerScript(code)`
18
+
19
+ ```ts
20
+ registerScript(code: string): Script
21
+ ```
22
+
23
+ Creates a Glide `Script`, registers it for process-exit release, and returns it.
24
+
25
+ ## Packaged Scripts
26
+
27
+ Cache scripts:
28
+
29
+ - `cache-delete-with-invalidation.lua`
30
+ - `cache-set-if-not-invalidated.lua`
31
+ - `get-value-with-ttl.lua`
32
+ - `get-values-with-ttl.lua`
33
+
34
+ Bloom filter scripts:
35
+
36
+ - `bloom-filter-add.lua`
37
+ - `bloom-filter-ensure-exists.lua`
38
+ - `bloom-filter-exists.lua`
39
+ - `bloom-filter-exists-if-ready.lua`
40
+ - `bloom-filter-mexists.lua`
41
+ - `bloom-filter-mexists-if-ready.lua`
42
+ - `bloom-filter-reserve.lua`
43
+
44
+ Dynamic config scripts:
45
+
46
+ - `dynamic-config-set-fields.lua`
47
+
48
+ Rate limiter scripts:
49
+
50
+ - `rate-limiter-add.lua`
51
+ - `rate-limiter-add-and-check.lua`
52
+ - `rate-limiter-get.lua`