valkyries 0.6.2 → 0.8.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
@@ -88,6 +88,21 @@ await flags.setField("enabled", true);
88
88
 
89
89
  Dynamic config stores fields in a Valkey hash and publishes changes over pub/sub.
90
90
 
91
+ ## Channel Pub/Sub
92
+
93
+ `valkyries/channel-pubsub` provides ephemeral, key-scoped pub/sub with caller-owned client
94
+ configuration and lifecycle. It is not re-exported from the package root.
95
+
96
+ ```ts
97
+ import { createChannelPubSub } from "valkyries/channel-pubsub";
98
+
99
+ const events = createChannelPubSub<{ status: string }>("exports", {
100
+ clientConfig: { addresses: [{ host: "localhost", port: 6379 }] },
101
+ });
102
+ ```
103
+
104
+ See [Channel pub/sub](docs/channel-pubsub.md) for lifecycle and error-handling details.
105
+
91
106
  ## Rate Limiter
92
107
 
93
108
  ```ts
@@ -113,6 +128,45 @@ const cache = new ValkeyCache({ prefix: "custom", ttlSeconds: 60, client });
113
128
 
114
129
  Call `closeValkeyClients()` to close package-managed clients.
115
130
 
131
+ ## Scan and Unlink Keys
132
+
133
+ Use `scanAndUnlinkKeys()` for cancellable, pattern-based cleanup with explicit scan and deletion
134
+ counts:
135
+
136
+ ```ts
137
+ import { scanAndUnlinkKeys } from "valkyries";
138
+
139
+ const result = await scanAndUnlinkKeys(client, "cache:*", {
140
+ matches: (key) => Buffer.from(key).toString("utf8").startsWith("cache:users:"),
141
+ signal: abortController.signal,
142
+ });
143
+ ```
144
+
145
+ The result reports `scannedKeys`, `matchedKeys`, and keys confirmed removed by `UNLINK` as
146
+ `unlinkedKeys`. `SCAN` is non-snapshot, so these counts can include duplicates or omit keys while
147
+ the keyspace changes.
148
+
149
+ ## Expire Keys Without a TTL
150
+
151
+ Use `expireKeysWithNoExpiry()` to apply a TTL only when matching keys still have no expiry:
152
+
153
+ ```ts
154
+ import { expireKeysWithNoExpiry } from "valkyries";
155
+
156
+ const result = await expireKeysWithNoExpiry(client, {
157
+ pattern: "cache:*",
158
+ ttl: 86_400,
159
+ shouldExpire: (key) => Buffer.from(key).toString("utf8").startsWith("cache:users:"),
160
+ signal: abortController.signal,
161
+ batchSize: 250,
162
+ });
163
+ ```
164
+
165
+ The result reports `scannedKeys`, `matchedKeys`, and `expiredKeys`. The helper uses `EXPIRE NX`,
166
+ so it never replaces a TTL that was added concurrently. `SCAN` is non-snapshot, so counts can
167
+ include duplicates or omit keys while the keyspace changes. `scanCount` is a SCAN hint, while
168
+ `batchSize` independently bounds each EXPIRE pipeline.
169
+
116
170
  ## Documentation
117
171
 
118
172
  - [Configuration](docs/configuration.md)
@@ -121,6 +175,7 @@ Call `closeValkeyClients()` to close package-managed clients.
121
175
  - [Cache](docs/cache.md)
122
176
  - [Bloom filters](docs/bloom-filters.md)
123
177
  - [Dynamic config](docs/dynamic-config.md)
178
+ - [Channel pub/sub](docs/channel-pubsub.md)
124
179
  - [Rate limiter](docs/rate-limiter.md)
125
180
  - [Idempotency keys](docs/idempotency-key.md)
126
181
  - [Conditional operations](docs/conditional-operations.md)
@@ -0,0 +1,13 @@
1
+ import type { GlideClient, GlideString } from "@valkey/valkey-glide";
2
+ export declare function decrement(counts: Map<string, number>, key: string): void;
3
+ export declare function removePending<T>(pending: Map<string, Set<T>>, key: string, state: T): void;
4
+ export declare function getOrCreateSet<T>(map: Map<string, Set<T>>, key: string): Set<T>;
5
+ export declare function removeHandler<T>(handlers: Map<string, Set<T>>, key: string, handler: T): void;
6
+ export declare function serializeForPublish<T>(serialize: (value: T) => string, value: T, report: (error: unknown) => void): string;
7
+ export declare function toError(error: unknown): Error;
8
+ export declare function reportError(onError: ((error: Error) => void) | undefined, error: unknown): void;
9
+ export declare function glideStringToString(value: GlideString): string;
10
+ export declare function assertChannelPart(name: string, value: string): void;
11
+ export declare function deliver<T>(handler: (value: T) => void, value: T, onError: (error: unknown) => void): void;
12
+ export declare function closeSubscriberClient(client: GlideClient, pattern: string, timeoutMs: number, report: (error: unknown) => void): Promise<void>;
13
+ //# sourceMappingURL=channel-pubsub-internals.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub-internals.d.mts","sourceRoot":"","sources":["../channel-pubsub-internals.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAErE,wBAAgB,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAIxE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAK1F;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAM/E;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAK7F;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,EAC/B,KAAK,EAAE,CAAC,EACR,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAC/B,MAAM,CAOR;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAE7C;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAI/F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE9D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,OAAO,CAAC,CAAC,EACvB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAC3B,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAChC,IAAI,CAMN;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAC/B,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,82 @@
1
+ export function decrement(counts, key) {
2
+ const count = (counts.get(key) ?? 1) - 1;
3
+ if (count <= 0)
4
+ counts.delete(key);
5
+ else
6
+ counts.set(key, count);
7
+ }
8
+ export function removePending(pending, key, state) {
9
+ const states = pending.get(key);
10
+ if (!states)
11
+ return;
12
+ states.delete(state);
13
+ if (states.size === 0)
14
+ pending.delete(key);
15
+ }
16
+ export function getOrCreateSet(map, key) {
17
+ const existing = map.get(key);
18
+ if (existing)
19
+ return existing;
20
+ const created = new Set();
21
+ map.set(key, created);
22
+ return created;
23
+ }
24
+ export function removeHandler(handlers, key, handler) {
25
+ const active = handlers.get(key);
26
+ if (!active)
27
+ return;
28
+ active.delete(handler);
29
+ if (active.size === 0)
30
+ handlers.delete(key);
31
+ }
32
+ export function serializeForPublish(serialize, value, report) {
33
+ try {
34
+ return serialize(value);
35
+ }
36
+ catch (error) {
37
+ report(error);
38
+ throw error;
39
+ }
40
+ }
41
+ export function toError(error) {
42
+ return error instanceof Error ? error : new Error(String(error));
43
+ }
44
+ export function reportError(onError, error) {
45
+ try {
46
+ onError?.(toError(error));
47
+ }
48
+ catch { }
49
+ }
50
+ export function glideStringToString(value) {
51
+ return typeof value === "string" ? value : value.toString();
52
+ }
53
+ export function assertChannelPart(name, value) {
54
+ if (value.length === 0 || /[?*[\]\\]/.test(value)) {
55
+ throw new Error(`${name} must be non-empty and cannot contain Valkey glob characters`);
56
+ }
57
+ }
58
+ export function deliver(handler, value, onError) {
59
+ try {
60
+ handler(value);
61
+ }
62
+ catch (error) {
63
+ onError(error);
64
+ }
65
+ }
66
+ export async function closeSubscriberClient(client, pattern, timeoutMs, report) {
67
+ try {
68
+ await client.punsubscribe(new Set([pattern]), timeoutMs);
69
+ }
70
+ catch (error) {
71
+ report(error);
72
+ }
73
+ finally {
74
+ try {
75
+ await Promise.resolve(client.close());
76
+ }
77
+ catch (error) {
78
+ report(error);
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=channel-pubsub-internals.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub-internals.mjs","sourceRoot":"","sources":["../channel-pubsub-internals.mts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { GlideClient } from "@valkey/valkey-glide";
2
+ type GlideClientConfig = Parameters<typeof GlideClient.createClient>[0];
3
+ export type ChannelPubSubHandler<T> = (value: T) => void;
4
+ export type ChannelSubscription<T> = {
5
+ setHandler(handler: ChannelPubSubHandler<T> | null): void;
6
+ close(): Promise<void>;
7
+ };
8
+ export type ChannelPubSub<T> = {
9
+ publish(key: string, value: T): Promise<void>;
10
+ subscribe(key: string): Promise<ChannelSubscription<T>>;
11
+ closeSubscriber(): Promise<void>;
12
+ close(): Promise<void>;
13
+ };
14
+ export type ChannelPubSubOptions<T> = {
15
+ clientConfig: GlideClientConfig;
16
+ createClient?: (config: GlideClientConfig) => Promise<GlideClient>;
17
+ serialize?: (value: T) => string;
18
+ deserialize?: (value: string) => T;
19
+ closeSubscriberWhenIdle?: boolean;
20
+ subscriberCloseTimeoutMs?: number;
21
+ onError?: (error: Error) => void;
22
+ };
23
+ export {};
24
+ //# sourceMappingURL=channel-pubsub-types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub-types.d.mts","sourceRoot":"","sources":["../channel-pubsub-types.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AACxE,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAEzD,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAAI;IACnC,UAAU,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI;IACpC,YAAY,EAAE,iBAAiB,CAAC;IAChC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACnE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;IACnC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=channel-pubsub-types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub-types.mjs","sourceRoot":"","sources":["../channel-pubsub-types.mts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { ChannelPubSub, ChannelPubSubOptions } from "./channel-pubsub-types.mts";
2
+ export type { ChannelPubSub, ChannelPubSubOptions, ChannelSubscription, } from "./channel-pubsub-types.mts";
3
+ export declare function createChannelPubSub<T>(prefix: string, options: ChannelPubSubOptions<T>): ChannelPubSub<T>;
4
+ //# sourceMappingURL=channel-pubsub.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub.d.mts","sourceRoot":"","sources":["../channel-pubsub.mts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,aAAa,EAEb,oBAAoB,EAErB,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AAEpC,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,aAAa,CAAC,CAAC,CAAC,CAqKlB"}
@@ -0,0 +1,184 @@
1
+ import { GlideClient, GlideClientConfiguration } from "@valkey/valkey-glide";
2
+ import { assertChannelPart, closeSubscriberClient, decrement, deliver, getOrCreateSet, glideStringToString, removePending, removeHandler, reportError, serializeForPublish, } from "./channel-pubsub-internals.mjs";
3
+ export function createChannelPubSub(prefix, options) {
4
+ assertChannelPart("prefix", prefix);
5
+ if (options.clientConfig.pubsubSubscriptions) {
6
+ throw new Error("clientConfig cannot include pubsubSubscriptions");
7
+ }
8
+ const closeTimeoutMs = options.subscriberCloseTimeoutMs ?? 5_000;
9
+ if (!Number.isSafeInteger(closeTimeoutMs) || closeTimeoutMs <= 0) {
10
+ throw new Error("subscriberCloseTimeoutMs must be a positive safe integer");
11
+ }
12
+ const report = (error) => reportError(options.onError, error);
13
+ const serialize = options.serialize ?? ((value) => JSON.stringify(value));
14
+ const deserialize = options.deserialize ?? ((value) => JSON.parse(value));
15
+ const createClient = options.createClient ?? ((config) => GlideClient.createClient(config));
16
+ const pattern = `${prefix}:*`;
17
+ const handlers = new Map();
18
+ const pending = new Map();
19
+ const counts = new Map();
20
+ let publisher;
21
+ let subscriber;
22
+ let subscriberClose;
23
+ let ownerClose;
24
+ let closed = false;
25
+ const assertOpen = () => {
26
+ if (closed)
27
+ throw new Error("Channel pub/sub is closed");
28
+ };
29
+ const totalCount = () => [...counts.values()].reduce((total, count) => total + count, 0);
30
+ const shutdownSubscriber = () => {
31
+ if (subscriberClose)
32
+ return subscriberClose;
33
+ const current = subscriber;
34
+ subscriber = undefined;
35
+ if (!current)
36
+ return Promise.resolve();
37
+ const closePromise = (async () => {
38
+ try {
39
+ await closeSubscriberClient(await current, pattern, closeTimeoutMs, report);
40
+ }
41
+ catch (error) {
42
+ report(error);
43
+ }
44
+ })();
45
+ subscriberClose = closePromise;
46
+ void closePromise.finally(() => {
47
+ subscriberClose = undefined;
48
+ });
49
+ return closePromise;
50
+ };
51
+ const closeSubscriberIfIdle = () => {
52
+ return options.closeSubscriberWhenIdle && totalCount() === 0
53
+ ? shutdownSubscriber()
54
+ : Promise.resolve();
55
+ };
56
+ const getPublisher = () => {
57
+ publisher ??= createClient(options.clientConfig).catch((error) => {
58
+ publisher = undefined;
59
+ throw error;
60
+ });
61
+ return publisher;
62
+ };
63
+ const receive = (message) => {
64
+ const messageChannel = glideStringToString(message.channel);
65
+ if (!messageChannel.startsWith(`${prefix}:`))
66
+ return;
67
+ const key = messageChannel.slice(prefix.length + 1);
68
+ if (!counts.has(key))
69
+ return;
70
+ let value;
71
+ try {
72
+ value = deserialize(glideStringToString(message.message));
73
+ }
74
+ catch (error) {
75
+ report(error);
76
+ return;
77
+ }
78
+ const currentHandlers = new Set(handlers.get(key));
79
+ for (const entry of currentHandlers)
80
+ deliver(entry.handler, value, report);
81
+ for (const state of pending.get(key) ?? [])
82
+ state.buffer.push(value);
83
+ };
84
+ const getSubscriber = async () => {
85
+ if (subscriberClose)
86
+ await subscriberClose;
87
+ assertOpen();
88
+ subscriber ??= createClient({
89
+ ...options.clientConfig,
90
+ pubsubSubscriptions: {
91
+ channelsAndPatterns: {
92
+ [GlideClientConfiguration.PubSubChannelModes.Pattern]: new Set([pattern]),
93
+ },
94
+ callback: receive,
95
+ context: undefined,
96
+ },
97
+ }).catch((error) => {
98
+ subscriber = undefined;
99
+ throw error;
100
+ });
101
+ return await subscriber;
102
+ };
103
+ return {
104
+ async publish(key, value) {
105
+ assertOpen();
106
+ assertChannelPart("key", key);
107
+ await (await getPublisher()).publish(serializeForPublish(serialize, value, report), `${prefix}:${key}`);
108
+ },
109
+ async subscribe(key) {
110
+ assertOpen();
111
+ assertChannelPart("key", key);
112
+ counts.set(key, (counts.get(key) ?? 0) + 1);
113
+ const state = { buffer: [] };
114
+ let handler;
115
+ let closePromise;
116
+ getOrCreateSet(pending, key).add(state);
117
+ const rollback = async () => {
118
+ removePending(pending, key, state);
119
+ if (handler)
120
+ removeHandler(handlers, key, handler);
121
+ decrement(counts, key);
122
+ await closeSubscriberIfIdle();
123
+ };
124
+ try {
125
+ await getSubscriber();
126
+ if (closed)
127
+ throw new Error("Channel pub/sub is closed");
128
+ }
129
+ catch (error) {
130
+ await rollback();
131
+ throw error;
132
+ }
133
+ return {
134
+ setHandler(nextHandler) {
135
+ if (closePromise)
136
+ return;
137
+ if (handler)
138
+ removeHandler(handlers, key, handler);
139
+ if (!nextHandler) {
140
+ handler = undefined;
141
+ getOrCreateSet(pending, key).add(state);
142
+ return;
143
+ }
144
+ removePending(pending, key, state);
145
+ handler = { handler: nextHandler };
146
+ getOrCreateSet(handlers, key).add(handler);
147
+ for (const value of state.buffer)
148
+ deliver(nextHandler, value, report);
149
+ state.buffer = [];
150
+ },
151
+ close() {
152
+ closePromise ??= rollback();
153
+ return closePromise;
154
+ },
155
+ };
156
+ },
157
+ closeSubscriber() {
158
+ if (totalCount() > 0) {
159
+ return Promise.reject(new Error("Cannot close subscriber while subscriptions are active"));
160
+ }
161
+ return shutdownSubscriber();
162
+ },
163
+ close() {
164
+ if (ownerClose)
165
+ return ownerClose;
166
+ closed = true;
167
+ ownerClose = (async () => {
168
+ await shutdownSubscriber();
169
+ const current = publisher;
170
+ publisher = undefined;
171
+ if (!current)
172
+ return;
173
+ try {
174
+ await Promise.resolve((await current).close());
175
+ }
176
+ catch (error) {
177
+ report(error);
178
+ }
179
+ })();
180
+ return ownerClose;
181
+ },
182
+ };
183
+ }
184
+ //# sourceMappingURL=channel-pubsub.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-pubsub.mjs","sourceRoot":"","sources":["../channel-pubsub.mts"],"names":[],"mappings":"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"}
package/dist/delete.d.mts CHANGED
@@ -1,4 +1,12 @@
1
+ import type { ScanAndUnlinkKeysOptions, ScanAndUnlinkKeysResult } from "./types.mts";
1
2
  import type { GlideClient } from "@valkey/valkey-glide";
2
3
  export declare function deleteKeysWithPrefix(client: GlideClient, pattern: string): Promise<void>;
3
4
  export declare function deleteKeysWithLiteralPrefixes(client: GlideClient, pattern: string, prefixes: readonly string[]): Promise<void>;
5
+ /**
6
+ * Scans keys matching a Valkey pattern and unlinks keys accepted by an optional predicate.
7
+ *
8
+ * SCAN is non-snapshot: concurrent writes can make the returned counts include duplicates,
9
+ * omit keys, or differ from the number of keys that exist when this function returns.
10
+ */
11
+ export declare function scanAndUnlinkKeys(client: GlideClient, pattern: string, { signal, matches }?: ScanAndUnlinkKeysOptions): Promise<ScanAndUnlinkKeysResult>;
4
12
  //# sourceMappingURL=delete.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.mts","sourceRoot":"","sources":["../delete.mts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,sBAAsB,CAAC;AAIrE,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9F;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"delete.d.mts","sourceRoot":"","sources":["../delete.mts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAIxD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9F;AAED,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,EAAE,MAAM,EAAE,OAAoB,EAAE,GAAE,wBAA6B,GAC9D,OAAO,CAAC,uBAAuB,CAAC,CA4BlC"}
package/dist/delete.mjs CHANGED
@@ -1,30 +1,47 @@
1
- import { Buffer } from "node:buffer";
2
1
  import { handleValkeyError } from "./errors.mjs";
2
+ import { glideStringToString, scanKeyPages, throwIfAborted } from "./scan.mjs";
3
3
  const DEFAULT_SCAN_COUNT = 500;
4
4
  export async function deleteKeysWithPrefix(client, pattern) {
5
- await scanAndUnlink(client, pattern);
5
+ await scanAndUnlinkKeys(client, pattern);
6
6
  }
7
7
  export async function deleteKeysWithLiteralPrefixes(client, pattern, prefixes) {
8
8
  const uniquePrefixes = [...new Set(prefixes)].filter((prefix) => typeof prefix === "string");
9
9
  if (uniquePrefixes.length === 0)
10
10
  return;
11
- await scanAndUnlink(client, pattern, (key) => uniquePrefixes.some((prefix) => key.startsWith(prefix)));
11
+ await scanAndUnlinkKeys(client, pattern, {
12
+ matches: (key) => uniquePrefixes.some((prefix) => glideStringToString(key).startsWith(prefix)),
13
+ });
12
14
  }
13
- async function scanAndUnlink(client, pattern, matches = () => true) {
15
+ /**
16
+ * Scans keys matching a Valkey pattern and unlinks keys accepted by an optional predicate.
17
+ *
18
+ * SCAN is non-snapshot: concurrent writes can make the returned counts include duplicates,
19
+ * omit keys, or differ from the number of keys that exist when this function returns.
20
+ */
21
+ export async function scanAndUnlinkKeys(client, pattern, { signal, matches = () => true } = {}) {
14
22
  try {
15
- let cursor = "0";
16
- do {
17
- const result = await client.scan(cursor, { match: pattern, count: DEFAULT_SCAN_COUNT });
18
- cursor = result[0];
19
- const keys = result[1]
20
- .map((key) => (typeof key === "string" ? key : Buffer.from(key).toString("utf8")))
21
- .filter(matches);
23
+ let scannedKeys = 0;
24
+ let matchedKeys = 0;
25
+ let unlinkedKeys = 0;
26
+ for await (const scanned of scanKeyPages(client, pattern, {
27
+ count: DEFAULT_SCAN_COUNT,
28
+ signal,
29
+ })) {
30
+ scannedKeys += scanned.length;
31
+ const keys = scanned.filter(matches);
32
+ throwIfAborted(signal);
33
+ matchedKeys += keys.length;
22
34
  if (keys.length > 0) {
23
- await client.unlink(keys);
35
+ throwIfAborted(signal);
36
+ unlinkedKeys += await client.unlink(keys);
37
+ throwIfAborted(signal);
24
38
  }
25
- } while (cursor !== "0");
39
+ }
40
+ return { scannedKeys, matchedKeys, unlinkedKeys };
26
41
  }
27
42
  catch (err) {
43
+ if (signal?.aborted && err === signal.reason)
44
+ throw err;
28
45
  handleValkeyError(err);
29
46
  throw err;
30
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"delete.mjs","sourceRoot":"","sources":["../delete.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGjD,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAmB,EAAE,OAAe;IAC7E,MAAM,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,MAAmB,EACnB,OAAe,EACf,QAA2B;IAE3B,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,CAAU,QAAQ,CAAC,CAAC,CAAC,MAAM,CAC3D,CAAC,MAAM,EAAoB,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,CACzD,CAAC;IACF,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAExC,MAAM,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAC3C,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CACxD,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,MAAmB,EACnB,OAAe,EACf,OAAO,GAA6B,GAAG,EAAE,CAAC,IAAI;IAE9C,IAAI,CAAC;QACH,IAAI,MAAM,GAAG,GAAG,CAAC;QAEjB,GAAG,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;YACxF,MAAM,GAAG,MAAM,CAAC,CAAC,CAAW,CAAC;YAC7B,MAAM,IAAI,GAAI,MAAM,CAAC,CAAC,CAAmB;iBACtC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;iBACjF,MAAM,CAAC,OAAO,CAAC,CAAC;YAEnB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC,QAAQ,MAAM,KAAK,GAAG,EAAE;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACvB,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"delete.mjs","sourceRoot":"","sources":["../delete.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAI/E,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAmB,EAAE,OAAe;IAC7E,MAAM,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,MAAmB,EACnB,OAAe,EACf,QAA2B;IAE3B,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,CAAU,QAAQ,CAAC,CAAC,CAAC,MAAM,CAC3D,CAAC,MAAM,EAAoB,EAAE,CAAC,OAAO,MAAM,KAAK,QAAQ,CACzD,CAAC;IACF,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAExC,MAAM,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE;QACvC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;KAC/F,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAmB,EACnB,OAAe,EACf,EAAE,MAAM,EAAE,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,EAAE,GAA6B,EAAE;IAE/D,IAAI,CAAC;QACH,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,IAAI,KAAK,EAAE,MAAM,OAAO,IAAI,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE;YACxD,KAAK,EAAE,kBAAkB;YACzB,MAAM;SACP,CAAC,EAAE,CAAC;YACH,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;YAC9B,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACrC,cAAc,CAAC,MAAM,CAAC,CAAC;YACvB,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC;YAE3B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,cAAc,CAAC,MAAM,CAAC,CAAC;gBACvB,YAAY,IAAI,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAC1C,cAAc,CAAC,MAAM,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QAED,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;IACpD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,MAAM,EAAE,OAAO,IAAI,GAAG,KAAK,MAAM,CAAC,MAAM;YAAE,MAAM,GAAG,CAAC;QACxD,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACvB,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type GlideClient } from "@valkey/valkey-glide";
2
+ import type { ExpireKeysWithNoExpiryOptions, ExpireKeysWithNoExpiryResult } from "./types.mts";
3
+ /**
4
+ * Sets a TTL only on matching keys that have no existing expiry.
5
+ *
6
+ * SCAN is non-snapshot: concurrent writes can make the returned counts include duplicates,
7
+ * omit keys, or differ from the number of keys that exist when this function returns.
8
+ */
9
+ export declare function expireKeysWithNoExpiry(client: GlideClient, { pattern, ttl, shouldExpire, signal, scanCount, batchSize, }: ExpireKeysWithNoExpiryOptions): Promise<ExpireKeysWithNoExpiryResult>;
10
+ //# sourceMappingURL=expiry.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.d.mts","sourceRoot":"","sources":["../expiry.mts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,WAAW,EAAoB,MAAM,sBAAsB,CAAC;AAGhG,OAAO,KAAK,EAAE,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAK/F;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,WAAW,EACnB,EACE,OAAO,EACP,GAAG,EACH,YAAyB,EACzB,MAAM,EACN,SAA8B,EAC9B,SAA8B,GAC/B,EAAE,6BAA6B,GAC/B,OAAO,CAAC,4BAA4B,CAAC,CAuBvC"}
@@ -0,0 +1,70 @@
1
+ import { Batch, ExpireOptions } from "@valkey/valkey-glide";
2
+ import { handleValkeyError } from "./errors.mjs";
3
+ import { scanKeyPages, throwIfAborted } from "./scan.mjs";
4
+ const DEFAULT_SCAN_COUNT = 500;
5
+ const DEFAULT_BATCH_SIZE = 500;
6
+ /**
7
+ * Sets a TTL only on matching keys that have no existing expiry.
8
+ *
9
+ * SCAN is non-snapshot: concurrent writes can make the returned counts include duplicates,
10
+ * omit keys, or differ from the number of keys that exist when this function returns.
11
+ */
12
+ export async function expireKeysWithNoExpiry(client, { pattern, ttl, shouldExpire = () => true, signal, scanCount = DEFAULT_SCAN_COUNT, batchSize = DEFAULT_BATCH_SIZE, }) {
13
+ validateExpirySweepOptions(ttl, scanCount, batchSize);
14
+ try {
15
+ let scannedKeys = 0;
16
+ let matchedKeys = 0;
17
+ let expiredKeys = 0;
18
+ for await (const scanned of scanKeyPages(client, pattern, { count: scanCount, signal })) {
19
+ scannedKeys += scanned.length;
20
+ const keys = scanned.filter(shouldExpire);
21
+ throwIfAborted(signal);
22
+ matchedKeys += keys.length;
23
+ expiredKeys += await expireKeys(client, keys, ttl, batchSize, signal);
24
+ }
25
+ return { scannedKeys, matchedKeys, expiredKeys };
26
+ }
27
+ catch (error) {
28
+ if (signal?.aborted && error === signal.reason)
29
+ throw error;
30
+ handleValkeyError(error);
31
+ throw error;
32
+ }
33
+ }
34
+ async function expireKeys(client, keys, ttl, batchSize, signal) {
35
+ let expiredKeys = 0;
36
+ for (let offset = 0; offset < keys.length; offset += batchSize) {
37
+ expiredKeys += await expireBatch(client, keys.slice(offset, offset + batchSize), ttl, signal);
38
+ }
39
+ return expiredKeys;
40
+ }
41
+ async function expireBatch(client, keys, ttl, signal) {
42
+ throwIfAborted(signal);
43
+ const batch = new Batch(false);
44
+ for (const key of keys) {
45
+ batch.expire(key, ttl, { expireOption: ExpireOptions.HasNoExpiry });
46
+ }
47
+ const results = await client.exec(batch, true);
48
+ throwIfAborted(signal);
49
+ if (!Array.isArray(results) || results.length !== keys.length) {
50
+ throw new Error("expireKeysWithNoExpiry: unexpected EXPIRE batch response");
51
+ }
52
+ let expiredKeys = 0;
53
+ for (const result of results) {
54
+ if (result === true)
55
+ expiredKeys += 1;
56
+ }
57
+ return expiredKeys;
58
+ }
59
+ function validateExpirySweepOptions(ttl, scanCount, batchSize) {
60
+ if (!Number.isSafeInteger(ttl) || ttl <= 0) {
61
+ throw new RangeError("expireKeysWithNoExpiry: ttl must be a positive safe integer");
62
+ }
63
+ if (!Number.isSafeInteger(scanCount) || scanCount <= 0) {
64
+ throw new RangeError("expireKeysWithNoExpiry: scanCount must be a positive safe integer");
65
+ }
66
+ if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
67
+ throw new RangeError("expireKeysWithNoExpiry: batchSize must be a positive safe integer");
68
+ }
69
+ }
70
+ //# sourceMappingURL=expiry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expiry.mjs","sourceRoot":"","sources":["../expiry.mts"],"names":[],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -13,5 +13,7 @@ export * from "./errors.mts";
13
13
  export * from "./key-normalization.mts";
14
14
  export * from "./cache-metrics.mts";
15
15
  export * from "./retry.mts";
16
- export type { RateLimiterAddAndCheckWindowsOptions, RateLimiterOptions, RateLimiterWindow, ValkeyBloomFilterOptions, ValkeyCacheOptions, ValkeyCacheMode, ValkeyCacheResponse, DynamicConfigField, DynamicConfigFieldType, DynamicConfigOptions, } from "./types.mts";
16
+ export { scanAndUnlinkKeys } from "./delete.mts";
17
+ export { expireKeysWithNoExpiry } from "./expiry.mts";
18
+ export type { RateLimiterAddAndCheckWindowsOptions, RateLimiterOptions, RateLimiterWindow, ValkeyBloomFilterOptions, ValkeyCacheOptions, ValkeyCacheMode, ValkeyCacheResponse, DynamicConfigField, DynamicConfigFieldType, DynamicConfigOptions, ScanAndUnlinkKeysOptions, ScanAndUnlinkKeysResult, ExpireKeysWithNoExpiryOptions, ExpireKeysWithNoExpiryResult, } from "./types.mts";
17
19
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,YAAY,EACV,oCAAoC,EACpC,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../index.mts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EACV,oCAAoC,EACpC,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,aAAa,CAAC"}
package/dist/index.mjs CHANGED
@@ -13,4 +13,6 @@ export * from "./errors.mjs";
13
13
  export * from "./key-normalization.mjs";
14
14
  export * from "./cache-metrics.mjs";
15
15
  export * from "./retry.mjs";
16
+ export { scanAndUnlinkKeys } from "./delete.mjs";
17
+ export { expireKeysWithNoExpiry } from "./expiry.mjs";
16
18
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../index.mts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../index.mts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type GlideClient, type GlideString } from "@valkey/valkey-glide";
2
+ type ScanKeyPagesOptions = {
3
+ count: number;
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function scanKeyPages(client: GlideClient, pattern: string, { count, signal }: ScanKeyPagesOptions): AsyncGenerator<GlideString[]>;
7
+ export declare function throwIfAborted(signal: AbortSignal | undefined): void;
8
+ export declare function glideStringToString(key: GlideString): string;
9
+ export {};
10
+ //# sourceMappingURL=scan.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.mts","sourceRoot":"","sources":["../scan.mts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnF,KAAK,mBAAmB,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,wBAAuB,YAAY,CACjC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,GACrC,cAAc,CAAC,WAAW,EAAE,CAAC,CAa/B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAEpE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAE5D"}
package/dist/scan.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { Decoder } from "@valkey/valkey-glide";
3
+ export async function* scanKeyPages(client, pattern, { count, signal }) {
4
+ let cursor = "0";
5
+ do {
6
+ throwIfAborted(signal);
7
+ const [nextCursor, keys] = await client.scan(cursor, {
8
+ match: pattern,
9
+ count,
10
+ decoder: Decoder.Bytes,
11
+ });
12
+ throwIfAborted(signal);
13
+ cursor = glideStringToString(nextCursor);
14
+ yield keys;
15
+ } while (cursor !== "0");
16
+ }
17
+ export function throwIfAborted(signal) {
18
+ if (signal?.aborted)
19
+ throw signal.reason;
20
+ }
21
+ export function glideStringToString(key) {
22
+ return typeof key === "string" ? key : Buffer.from(key).toString("utf8");
23
+ }
24
+ //# sourceMappingURL=scan.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.mjs","sourceRoot":"","sources":["../scan.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAsC,MAAM,sBAAsB,CAAC;AAOnF,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,YAAY,CACjC,MAAmB,EACnB,OAAe,EACf,EAAE,KAAK,EAAE,MAAM,EAAuB;IAEtC,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,GAAG,CAAC;QACF,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE;YACnD,KAAK,EAAE,OAAO;YACd,KAAK;YACL,OAAO,EAAE,OAAO,CAAC,KAAK;SACvB,CAAC,CAAC;QACH,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC;IACb,CAAC,QAAQ,MAAM,KAAK,GAAG,EAAE;AAC3B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAA+B;IAC5D,IAAI,MAAM,EAAE,OAAO;QAAE,MAAM,MAAM,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAgB;IAClD,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC3E,CAAC"}
package/dist/types.d.mts CHANGED
@@ -1,4 +1,40 @@
1
- import type { GlideClient } from "@valkey/valkey-glide";
1
+ import type { GlideClient, GlideString } from "@valkey/valkey-glide";
2
+ export type ScanAndUnlinkKeysOptions = {
3
+ /** Stops before or after each SCAN and UNLINK command boundary. */
4
+ signal?: AbortSignal;
5
+ /** Selects scanned string or Buffer keys to unlink. Defaults to every scanned key. */
6
+ matches?: (key: GlideString) => boolean;
7
+ };
8
+ export type ScanAndUnlinkKeysResult = {
9
+ /** Number of keys returned by SCAN, including any duplicates from its non-snapshot iteration. */
10
+ scannedKeys: number;
11
+ /** Number of scanned keys accepted by matches. */
12
+ matchedKeys: number;
13
+ /** Number of keys confirmed by UNLINK as removed. */
14
+ unlinkedKeys: number;
15
+ };
16
+ export type ExpireKeysWithNoExpiryOptions = {
17
+ /** Valkey SCAN pattern for keys to evaluate. */
18
+ pattern: string;
19
+ /** TTL in seconds to apply only where the key has no expiry. */
20
+ ttl: number;
21
+ /** Selects scanned string or Buffer keys to expire. Defaults to every scanned key. */
22
+ shouldExpire?: (key: GlideString) => boolean;
23
+ /** Stops before or after each SCAN and EXPIRE-batch command boundary. */
24
+ signal?: AbortSignal;
25
+ /** SCAN COUNT hint. Defaults to 500. */
26
+ scanCount?: number;
27
+ /** Maximum number of EXPIRE commands in one non-atomic pipeline. Defaults to 500. */
28
+ batchSize?: number;
29
+ };
30
+ export type ExpireKeysWithNoExpiryResult = {
31
+ /** Number of keys returned by SCAN, including any duplicates from its non-snapshot iteration. */
32
+ scannedKeys: number;
33
+ /** Number of scanned keys accepted by shouldExpire. */
34
+ matchedKeys: number;
35
+ /** Number of keys confirmed by EXPIRE NX as newly given a TTL. */
36
+ expiredKeys: number;
37
+ };
2
38
  export type ValkeyClientsOption = {
3
39
  cacheClient?: GlideClient;
4
40
  rateLimiterClient?: GlideClient;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../types.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../types.mts"],"names":[],"mappings":"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"}
package/docs/api.md CHANGED
@@ -7,6 +7,7 @@ This is the entry point for the public `valkyries` API. Detailed API documentati
7
7
  - [`ValkeyCache`](cache.md) - read-through cache helpers, direct get/set APIs, invalidation, stale refresh, and Bloom-filter integration.
8
8
  - [`ValkeyBloomFilter`](bloom-filters.md) - Bloom filter lookup, writes, readiness markers, streaming rebuilds, and key helpers.
9
9
  - [`DynamicConfig`](dynamic-config.md) - distributed runtime fields backed by Valkey hashes and pub/sub updates.
10
+ - [Channel pub/sub](channel-pubsub.md) - ephemeral, caller-managed key-scoped pub/sub.
10
11
  - [`RateLimiter`](rate-limiter.md) - sliding-window rate limiting backed by sorted sets and Lua scripts.
11
12
  - [Idempotency keys](idempotency-key.md) - consume-once values and token-fenced idempotency-key lifecycle helpers.
12
13
 
@@ -29,6 +30,8 @@ import {
29
30
  getAndDelete,
30
31
  reserveIdempotencyKey,
31
32
  closeValkeyClients,
33
+ scanAndUnlinkKeys,
34
+ expireKeysWithNoExpiry,
32
35
  } from "valkyries";
33
36
  ```
34
37
 
@@ -40,5 +43,6 @@ Supported subpath exports:
40
43
  - `valkyries/dynamic-config`
41
44
  - `valkyries/idempotency-key`
42
45
  - `valkyries/rate-limiter`
46
+ - `valkyries/channel-pubsub`
43
47
 
44
48
  `valkyries` is ESM-only and requires Node.js 24 or newer.
@@ -0,0 +1,59 @@
1
+ # Channel Pub/Sub
2
+
3
+ `createChannelPubSub()` creates a dedicated publisher and pattern subscriber for messages scoped by
4
+ an application-defined prefix and key. It is an ephemeral delivery primitive: messages are not
5
+ persisted, and subscribers do not receive messages published before their Valkey subscription is
6
+ active.
7
+
8
+ ## Import
9
+
10
+ ```ts
11
+ import { createChannelPubSub, type ChannelSubscription } from "valkyries/channel-pubsub";
12
+ ```
13
+
14
+ This subpath is intentionally not exported from `valkyries`; importing it never initializes a
15
+ package-managed client.
16
+
17
+ ## Create a channel
18
+
19
+ ```ts
20
+ const events = createChannelPubSub<{ state: string }>("exports", {
21
+ clientConfig: { addresses: [{ host: "localhost", port: 6379 }] },
22
+ onError: (error) => logger.error(error),
23
+ });
24
+ ```
25
+
26
+ `clientConfig` is passed to `GlideClient.createClient()`. Supply `createClient` to own connection
27
+ creation, for example when an application injects clients in tests. `serialize` and `deserialize`
28
+ default to JSON. `onError` receives codec, handler, and subscriber-shutdown failures; publish and
29
+ initial subscriber-connection failures reject their calling promise.
30
+
31
+ `clientConfig` must not include `pubsubSubscriptions`; this API owns the subscriber's pattern and
32
+ callback. `onError` failures are contained so consumer and cleanup isolation is preserved.
33
+
34
+ Prefixes and keys must be non-empty and cannot contain Valkey glob characters (`?`, `*`, `[`, `]`,
35
+ or `\`). This prevents a prefix from widening the subscriber pattern unintentionally.
36
+
37
+ `closeSubscriberWhenIdle` closes the subscriber when the final subscription closes. Set
38
+ `subscriberCloseTimeoutMs` to a positive safe integer to bound its `PUNSUBSCRIBE` request; it
39
+ defaults to `5000`.
40
+
41
+ ## Use and close
42
+
43
+ ```ts
44
+ const subscription: ChannelSubscription<{ state: string }> = await events.subscribe(exportId);
45
+ subscription.setHandler((event) => send(event));
46
+ await events.publish(exportId, { state: "ready" });
47
+ await subscription.close();
48
+ await events.close();
49
+ ```
50
+
51
+ Subscriptions buffer messages received before a handler is installed and replay them when it is.
52
+ Multiple subscriptions for one key each receive the message. Calling `setHandler()` replaces the
53
+ previous handler. Both subscription `close()` and owner `close()` are idempotent. Owner `close()`
54
+ also closes the lazy publisher; future `publish()` and `subscribe()` calls reject.
55
+
56
+ `closeSubscriber()` closes only an idle subscriber connection. It rejects while subscriptions are
57
+ active, preventing live handles from silently losing delivery. `close()` is the explicit owner
58
+ shutdown operation and closes active subscriptions' connection safely; it coalesces concurrent
59
+ callers until cleanup completes.
package/docs/utilities.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  These normalization helpers are exported from the package root.
4
4
 
5
+ ## `scanAndUnlinkKeys(client, pattern, options)`
6
+
7
+ ```ts
8
+ scanAndUnlinkKeys(
9
+ client: GlideClient,
10
+ pattern: string,
11
+ options?: {
12
+ signal?: AbortSignal;
13
+ matches?: (key: GlideString) => boolean;
14
+ },
15
+ ): Promise<{ scannedKeys: number; matchedKeys: number; unlinkedKeys: number }>
16
+ ```
17
+
18
+ Scans with `COUNT 500`, unlinks every scanned key accepted by `matches`, and returns the number of
19
+ keys scanned, predicate-matched, and confirmed removed by `UNLINK`. Keys are provided to `matches`
20
+ and `UNLINK` as their original string or `Buffer` `GlideString` values.
21
+
22
+ `SCAN` is non-snapshot: concurrent writes can make counts include duplicates, omit keys, or differ
23
+ from the keys that exist when the call returns. The optional `signal` is checked before and after
24
+ every SCAN and UNLINK boundary; its exact abort reason is rethrown without reporting it to the
25
+ configured Valkey error handler.
26
+
27
+ ## `expireKeysWithNoExpiry(client, options)`
28
+
29
+ ```ts
30
+ expireKeysWithNoExpiry(
31
+ client: GlideClient,
32
+ options: {
33
+ pattern: string;
34
+ ttl: number;
35
+ shouldExpire?: (key: GlideString) => boolean;
36
+ signal?: AbortSignal;
37
+ scanCount?: number;
38
+ batchSize?: number;
39
+ },
40
+ ): Promise<{ scannedKeys: number; matchedKeys: number; expiredKeys: number }>
41
+ ```
42
+
43
+ Scans matching keys and pipelines `EXPIRE NX` commands for every key accepted by `shouldExpire`.
44
+ `EXPIRE NX` atomically sets the TTL only when a key has no existing expiry, so a concurrent TTL
45
+ change is never overwritten. `scanCount` defaults to 500 as a SCAN hint. `batchSize` separately
46
+ defaults to 500 and bounds one non-atomic pipeline.
47
+
48
+ `SCAN` is non-snapshot: concurrent writes can make counts include duplicates, omit keys, or differ
49
+ from the keys that exist when the call returns. The optional `signal` is checked before and after
50
+ each SCAN and EXPIRE-batch boundary; its exact abort reason is rethrown without reporting it to the
51
+ configured Valkey error handler.
52
+
5
53
  ## `normalizeKey(key)`
6
54
 
7
55
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valkyries",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Valkey utilities for caches, bloom filters, dynamic configuration, and rate limiting.",
5
5
  "homepage": "https://github.com/jonathanong/valkyries#readme",
6
6
  "bugs": {
@@ -32,6 +32,11 @@
32
32
  "import": "./dist/cache.mjs",
33
33
  "default": "./dist/cache.mjs"
34
34
  },
35
+ "./channel-pubsub": {
36
+ "types": "./dist/channel-pubsub.d.mts",
37
+ "import": "./dist/channel-pubsub.mjs",
38
+ "default": "./dist/channel-pubsub.mjs"
39
+ },
35
40
  "./conditional": {
36
41
  "types": "./dist/conditional.d.mts",
37
42
  "import": "./dist/conditional.mjs",
@@ -65,9 +70,9 @@
65
70
  "@types/node": "^26.1.0",
66
71
  "@vitest/coverage-v8": "^4.1.6",
67
72
  "github-actionlint": "^1.7.12",
68
- "oxfmt": "^0.59.0",
73
+ "oxfmt": "^0.63.0",
69
74
  "oxlint": "^1.65.0",
70
- "oxlint-tsgolint": "^0.25.0",
75
+ "oxlint-tsgolint": "^7.0.2001",
71
76
  "tsx": "^4.22.1",
72
77
  "typescript": "^7.0.2",
73
78
  "vitest": "^4.1.6"