@frontmcp/utils 1.10.15 → 1.11.0-rc.1
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 +1 -0
- package/browser/async-context/browser-async-context.mjs +177 -0
- package/browser/async-context/index.mjs +15 -0
- package/browser/async-context/overlap-error.mjs +10 -0
- package/browser/class-brand/class-brand.mjs +27 -0
- package/browser/class-brand/index.mjs +6 -0
- package/browser/content/content.mjs +151 -0
- package/browser/content/index.mjs +8 -0
- package/browser/crypto/browser.mjs +103 -0
- package/browser/crypto/encrypted-blob.mjs +119 -0
- package/browser/crypto/hmac-signing.mjs +70 -0
- package/browser/crypto/index.mjs +281 -0
- package/browser/crypto/jwt-alg.mjs +40 -0
- package/browser/crypto/key-persistence/factory.mjs +134 -0
- package/browser/crypto/key-persistence/index.mjs +26 -0
- package/browser/crypto/key-persistence/key-persistence.mjs +185 -0
- package/browser/crypto/key-persistence/schemas.mjs +82 -0
- package/browser/crypto/pkce/index.mjs +25 -0
- package/browser/crypto/pkce/pkce.mjs +59 -0
- package/browser/crypto/runtime.mjs +17 -0
- package/browser/crypto/secret-persistence/index.mjs +29 -0
- package/browser/crypto/secret-persistence/persistence.mjs +181 -0
- package/browser/crypto/secret-persistence/schema.mjs +40 -0
- package/browser/crypto/web-rsa.mjs +36 -0
- package/browser/env/browser-env.mjs +56 -0
- package/browser/env/browser-runtime-context.mjs +85 -0
- package/browser/env/build-target.mjs +25 -0
- package/browser/env/edge-scope.mjs +17 -0
- package/browser/env/index.mjs +47 -0
- package/browser/env/provider.mjs +46 -0
- package/browser/escape/escape.mjs +43 -0
- package/browser/escape/index.mjs +9 -0
- package/browser/event-emitter/browser-event-emitter.mjs +49 -0
- package/browser/event-emitter/index.mjs +5 -0
- package/browser/fs/fs.mjs +227 -0
- package/browser/fs/index.mjs +65 -0
- package/browser/http/cookie.mjs +85 -0
- package/browser/http/http.mjs +18 -0
- package/browser/http/index.mjs +11 -0
- package/browser/index.mjs +526 -0
- package/browser/llm/index.mjs +5 -0
- package/browser/llm/llm-tool-handlers.mjs +55 -0
- package/browser/machine-id/index.mjs +7 -0
- package/browser/machine-id/machine-id.mjs +108 -0
- package/browser/naming/index.mjs +10 -0
- package/browser/naming/naming.mjs +68 -0
- package/browser/net/index.mjs +10 -0
- package/browser/net/ip-address.mjs +128 -0
- package/browser/net/redirect.mjs +7 -0
- package/browser/path/browser-path.mjs +47 -0
- package/browser/path/index.mjs +24 -0
- package/browser/path/path.mjs +13 -0
- package/browser/process/child-process.mjs +23 -0
- package/browser/process/index.mjs +15 -0
- package/browser/process/owned-process.mjs +175 -0
- package/browser/process/process-tree.mjs +149 -0
- package/browser/process/process-watch.mjs +48 -0
- package/browser/redaction/index.mjs +6 -0
- package/browser/redaction/redaction.mjs +74 -0
- package/browser/regex/index.mjs +3 -0
- package/browser/regex/patterns.mjs +167 -0
- package/browser/regex/safe-regex.mjs +93 -0
- package/browser/serialization/index.mjs +5 -0
- package/browser/serialization/serialization.mjs +22 -0
- package/browser/stdio-bridge/errors.mjs +27 -0
- package/browser/stdio-bridge/index.mjs +24 -0
- package/browser/stdio-bridge/list-changed.mjs +21 -0
- package/browser/stdio-bridge/state-machine.mjs +211 -0
- package/browser/stdio-bridge/stdio-framer.mjs +91 -0
- package/browser/stdio-bridge/upstream-client.mjs +189 -0
- package/browser/storage/adapters/base.mjs +139 -0
- package/browser/storage/adapters/cloudflare-kv.mjs +149 -0
- package/browser/storage/adapters/filesystem.mjs +279 -0
- package/browser/storage/adapters/index.mjs +23 -0
- package/browser/storage/adapters/indexeddb.mjs +198 -0
- package/browser/storage/adapters/localstorage.mjs +222 -0
- package/browser/storage/adapters/memory.mjs +350 -0
- package/browser/storage/adapters/redis.mjs +497 -0
- package/browser/storage/adapters/upstash.mjs +299 -0
- package/browser/storage/adapters/vercel-kv.mjs +226 -0
- package/browser/storage/change-feed.mjs +189 -0
- package/browser/storage/encrypted-typed-storage.mjs +329 -0
- package/browser/storage/errors.mjs +92 -0
- package/browser/storage/factory.mjs +155 -0
- package/browser/storage/index.mjs +96 -0
- package/browser/storage/namespace.mjs +186 -0
- package/browser/storage/redis-error-listener.mjs +32 -0
- package/browser/storage/redis-url.mjs +66 -0
- package/browser/storage/typed-storage.mjs +171 -0
- package/browser/storage/utils/compare-and-delete.mjs +9 -0
- package/browser/storage/utils/index.mjs +24 -0
- package/browser/storage/utils/pattern.mjs +68 -0
- package/browser/storage/utils/ttl.mjs +48 -0
- package/browser/uri/index.mjs +19 -0
- package/browser/uri/uri-template.mjs +86 -0
- package/browser/uri/uri-validation.mjs +18 -0
- package/content/content.d.ts +8 -0
- package/content/index.d.ts +1 -1
- package/crypto/browser.d.ts +1 -13
- package/crypto/browser.js +1 -1
- package/crypto/node.js +1 -1
- package/crypto/web-rsa.d.ts +13 -0
- package/env/browser-runtime-context.js +1 -1
- package/env/runtime-context.js +1 -1
- package/esm/crypto/browser.mjs +1 -1
- package/esm/crypto/node.mjs +1 -1
- package/esm/env/browser-runtime-context.mjs +1 -1
- package/esm/env/runtime-context.mjs +1 -1
- package/esm/index.mjs +1 -1
- package/fs/fs.d.ts +45 -4
- package/fs/index.d.ts +1 -1
- package/index.d.ts +10 -4
- package/index.js +1 -1
- package/package.json +24 -8
- package/process/child-process.d.ts +2 -0
- package/process/index.d.ts +6 -0
- package/process/owned-process.d.ts +59 -0
- package/process/process-tree.d.ts +78 -0
- package/process/process-watch.d.ts +41 -0
- package/redaction/index.d.ts +2 -0
- package/redaction/redaction.d.ts +24 -0
- package/stdio-bridge/errors.d.ts +23 -0
- package/stdio-bridge/index.d.ts +9 -0
- package/stdio-bridge/list-changed.d.ts +3 -0
- package/stdio-bridge/state-machine.d.ts +61 -0
- package/stdio-bridge/stdio-framer.d.ts +49 -0
- package/stdio-bridge/types.d.ts +10 -0
- package/stdio-bridge/upstream-client.d.ts +66 -0
- package/storage/adapters/base.d.ts +2 -0
- package/storage/adapters/memory.d.ts +1 -0
- package/storage/adapters/redis.d.ts +3 -0
- package/storage/change-feed.d.ts +47 -0
- package/storage/factory.d.ts +6 -0
- package/storage/index.d.ts +3 -1
- package/storage/namespace.d.ts +2 -0
- package/storage/types.d.ts +46 -1
- package/storage/utils/compare-and-delete.d.ts +2 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/upstash.ts
|
|
2
|
+
import { StorageConfigError, StorageConnectionError } from "../errors.mjs";
|
|
3
|
+
import { COMPARE_AND_DELETE_SCRIPT, COMPARE_AND_EXPIRE_SCRIPT } from "../utils/compare-and-delete.mjs";
|
|
4
|
+
import { validateTTL } from "../utils/ttl.mjs";
|
|
5
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
6
|
+
async function loadUpstashRedis() {
|
|
7
|
+
try {
|
|
8
|
+
const mod = await import("@upstash/redis");
|
|
9
|
+
const resolved = typeof mod.Redis === "function" ? mod : mod.default;
|
|
10
|
+
if (!resolved || typeof resolved.Redis !== "function") throw new Error("Redis missing");
|
|
11
|
+
return resolved;
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"@upstash/redis is required for Upstash storage adapter. Install it with: npm install @upstash/redis"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var PUBSUB_POLL_INTERVAL_MS = 100;
|
|
19
|
+
var UpstashStorageAdapter = class extends BaseStorageAdapter {
|
|
20
|
+
backendName = "upstash";
|
|
21
|
+
client;
|
|
22
|
+
options;
|
|
23
|
+
keyPrefix;
|
|
24
|
+
pubSubEnabled;
|
|
25
|
+
// Pub/sub state
|
|
26
|
+
subscriptionHandlers = /* @__PURE__ */ new Map();
|
|
27
|
+
pollingIntervals = /* @__PURE__ */ new Map();
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
super();
|
|
30
|
+
const url = options.url ?? process.env["UPSTASH_REDIS_REST_URL"];
|
|
31
|
+
const token = options.token ?? process.env["UPSTASH_REDIS_REST_TOKEN"];
|
|
32
|
+
if (!url || !token) {
|
|
33
|
+
throw new StorageConfigError(
|
|
34
|
+
"upstash",
|
|
35
|
+
"UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be provided via options or environment variables."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
this.options = { ...options, url, token };
|
|
39
|
+
this.keyPrefix = options.keyPrefix ?? "";
|
|
40
|
+
this.pubSubEnabled = options.enablePubSub ?? false;
|
|
41
|
+
}
|
|
42
|
+
// ============================================
|
|
43
|
+
// Connection Lifecycle
|
|
44
|
+
// ============================================
|
|
45
|
+
async connect() {
|
|
46
|
+
if (this.connected) return;
|
|
47
|
+
try {
|
|
48
|
+
const { Redis } = await loadUpstashRedis();
|
|
49
|
+
const url = this.options.url;
|
|
50
|
+
const token = this.options.token;
|
|
51
|
+
if (!url || !token) {
|
|
52
|
+
throw new StorageConfigError("upstash", "URL and token are required");
|
|
53
|
+
}
|
|
54
|
+
this.client = new Redis({ url, token, automaticDeserialization: false });
|
|
55
|
+
await this.client.exists("__healthcheck__");
|
|
56
|
+
this.connected = true;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw new StorageConnectionError(
|
|
59
|
+
"Failed to connect to Upstash Redis",
|
|
60
|
+
e instanceof Error ? e : void 0,
|
|
61
|
+
"upstash"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async disconnect() {
|
|
66
|
+
if (!this.connected) return;
|
|
67
|
+
for (const interval of this.pollingIntervals.values()) {
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
}
|
|
70
|
+
this.pollingIntervals.clear();
|
|
71
|
+
this.subscriptionHandlers.clear();
|
|
72
|
+
this.client = void 0;
|
|
73
|
+
this.connected = false;
|
|
74
|
+
}
|
|
75
|
+
async ping() {
|
|
76
|
+
if (!this.client) return false;
|
|
77
|
+
try {
|
|
78
|
+
await this.client.exists("__healthcheck__");
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ============================================
|
|
85
|
+
// Connection Helpers
|
|
86
|
+
// ============================================
|
|
87
|
+
/**
|
|
88
|
+
* Get the connected Upstash client, throwing if not connected.
|
|
89
|
+
*/
|
|
90
|
+
getConnectedClient() {
|
|
91
|
+
this.ensureConnected();
|
|
92
|
+
if (!this.client) {
|
|
93
|
+
throw new StorageConnectionError("Upstash client not connected", void 0, "upstash");
|
|
94
|
+
}
|
|
95
|
+
return this.client;
|
|
96
|
+
}
|
|
97
|
+
// ============================================
|
|
98
|
+
// Core Operations
|
|
99
|
+
// ============================================
|
|
100
|
+
async get(key) {
|
|
101
|
+
return this.getConnectedClient().get(this.prefixKey(key));
|
|
102
|
+
}
|
|
103
|
+
async doSet(key, value, options) {
|
|
104
|
+
const client = this.getConnectedClient();
|
|
105
|
+
const prefixedKey = this.prefixKey(key);
|
|
106
|
+
const setOptions = {};
|
|
107
|
+
if (options?.ttlSeconds) {
|
|
108
|
+
setOptions.ex = options.ttlSeconds;
|
|
109
|
+
}
|
|
110
|
+
if (options?.ifNotExists) {
|
|
111
|
+
setOptions.nx = true;
|
|
112
|
+
} else if (options?.ifExists) {
|
|
113
|
+
setOptions.xx = true;
|
|
114
|
+
}
|
|
115
|
+
await client.set(prefixedKey, value, Object.keys(setOptions).length > 0 ? setOptions : void 0);
|
|
116
|
+
}
|
|
117
|
+
async delete(key) {
|
|
118
|
+
const result = await this.getConnectedClient().del(this.prefixKey(key));
|
|
119
|
+
return result > 0;
|
|
120
|
+
}
|
|
121
|
+
async deleteIfEquals(key, expectedValue) {
|
|
122
|
+
const deleted = await this.getConnectedClient().eval(
|
|
123
|
+
COMPARE_AND_DELETE_SCRIPT,
|
|
124
|
+
[this.prefixKey(key)],
|
|
125
|
+
[expectedValue]
|
|
126
|
+
);
|
|
127
|
+
return deleted === 1;
|
|
128
|
+
}
|
|
129
|
+
async expireIfEquals(key, expectedValue, ttlSeconds) {
|
|
130
|
+
validateTTL(ttlSeconds);
|
|
131
|
+
const extended = await this.getConnectedClient().eval(
|
|
132
|
+
COMPARE_AND_EXPIRE_SCRIPT,
|
|
133
|
+
[this.prefixKey(key)],
|
|
134
|
+
[expectedValue, String(ttlSeconds)]
|
|
135
|
+
);
|
|
136
|
+
return extended === 1;
|
|
137
|
+
}
|
|
138
|
+
async exists(key) {
|
|
139
|
+
const result = await this.getConnectedClient().exists(this.prefixKey(key));
|
|
140
|
+
return result > 0;
|
|
141
|
+
}
|
|
142
|
+
// ============================================
|
|
143
|
+
// Batch Operations
|
|
144
|
+
// ============================================
|
|
145
|
+
async mget(keys) {
|
|
146
|
+
if (keys.length === 0) return [];
|
|
147
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
148
|
+
return this.getConnectedClient().mget(...prefixedKeys);
|
|
149
|
+
}
|
|
150
|
+
async mdelete(keys) {
|
|
151
|
+
if (keys.length === 0) return 0;
|
|
152
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
153
|
+
return this.getConnectedClient().del(...prefixedKeys);
|
|
154
|
+
}
|
|
155
|
+
// ============================================
|
|
156
|
+
// TTL Operations
|
|
157
|
+
// ============================================
|
|
158
|
+
async expire(key, ttlSeconds) {
|
|
159
|
+
validateTTL(ttlSeconds);
|
|
160
|
+
const result = await this.getConnectedClient().expire(this.prefixKey(key), ttlSeconds);
|
|
161
|
+
return result === 1;
|
|
162
|
+
}
|
|
163
|
+
async ttl(key) {
|
|
164
|
+
const result = await this.getConnectedClient().ttl(this.prefixKey(key));
|
|
165
|
+
if (result === -2) return null;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
// ============================================
|
|
169
|
+
// Key Enumeration
|
|
170
|
+
// ============================================
|
|
171
|
+
async keys(pattern = "*") {
|
|
172
|
+
const client = this.getConnectedClient();
|
|
173
|
+
const prefixedPattern = this.prefixKey(pattern);
|
|
174
|
+
const result = [];
|
|
175
|
+
let cursor = 0;
|
|
176
|
+
do {
|
|
177
|
+
const [nextCursor, keys] = await client.scan(cursor, {
|
|
178
|
+
match: prefixedPattern,
|
|
179
|
+
count: 100
|
|
180
|
+
});
|
|
181
|
+
const parsedCursor = typeof nextCursor === "string" ? parseInt(nextCursor, 10) : nextCursor;
|
|
182
|
+
cursor = Number.isNaN(parsedCursor) ? 0 : parsedCursor;
|
|
183
|
+
for (const key of keys) {
|
|
184
|
+
result.push(this.unprefixKey(key));
|
|
185
|
+
}
|
|
186
|
+
} while (cursor !== 0);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
// ============================================
|
|
190
|
+
// Atomic Operations
|
|
191
|
+
// ============================================
|
|
192
|
+
async incr(key) {
|
|
193
|
+
return this.getConnectedClient().incr(this.prefixKey(key));
|
|
194
|
+
}
|
|
195
|
+
async decr(key) {
|
|
196
|
+
return this.getConnectedClient().decr(this.prefixKey(key));
|
|
197
|
+
}
|
|
198
|
+
async incrBy(key, amount) {
|
|
199
|
+
return this.getConnectedClient().incrby(this.prefixKey(key), amount);
|
|
200
|
+
}
|
|
201
|
+
// ============================================
|
|
202
|
+
// Pub/Sub (via list-based polling)
|
|
203
|
+
// ============================================
|
|
204
|
+
supportsPubSub() {
|
|
205
|
+
return this.pubSubEnabled;
|
|
206
|
+
}
|
|
207
|
+
async publish(channel, message) {
|
|
208
|
+
if (!this.pubSubEnabled) {
|
|
209
|
+
return super.publish(channel, message);
|
|
210
|
+
}
|
|
211
|
+
const prefixedChannel = this.prefixKey(`__pubsub__:${channel}`);
|
|
212
|
+
const listKey = `${prefixedChannel}:queue`;
|
|
213
|
+
await this.getConnectedClient().lpush(listKey, message);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
async subscribe(channel, handler) {
|
|
217
|
+
const client = this.getConnectedClient();
|
|
218
|
+
if (!this.pubSubEnabled) {
|
|
219
|
+
return super.subscribe(channel, handler);
|
|
220
|
+
}
|
|
221
|
+
const prefixedChannel = this.prefixKey(`__pubsub__:${channel}`);
|
|
222
|
+
if (!this.subscriptionHandlers.has(prefixedChannel)) {
|
|
223
|
+
this.subscriptionHandlers.set(prefixedChannel, /* @__PURE__ */ new Set());
|
|
224
|
+
const listKey = `${prefixedChannel}:queue`;
|
|
225
|
+
const interval = setInterval(async () => {
|
|
226
|
+
try {
|
|
227
|
+
const message = await client.rpop(listKey);
|
|
228
|
+
if (message) {
|
|
229
|
+
const handlers2 = this.subscriptionHandlers.get(prefixedChannel);
|
|
230
|
+
if (handlers2) {
|
|
231
|
+
for (const h of handlers2) {
|
|
232
|
+
try {
|
|
233
|
+
h(message, channel);
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}, PUBSUB_POLL_INTERVAL_MS);
|
|
242
|
+
if (interval.unref) {
|
|
243
|
+
interval.unref();
|
|
244
|
+
}
|
|
245
|
+
this.pollingIntervals.set(prefixedChannel, interval);
|
|
246
|
+
}
|
|
247
|
+
const handlers = this.subscriptionHandlers.get(prefixedChannel);
|
|
248
|
+
if (handlers) {
|
|
249
|
+
handlers.add(handler);
|
|
250
|
+
}
|
|
251
|
+
return async () => {
|
|
252
|
+
const handlers2 = this.subscriptionHandlers.get(prefixedChannel);
|
|
253
|
+
if (handlers2) {
|
|
254
|
+
handlers2.delete(handler);
|
|
255
|
+
if (handlers2.size === 0) {
|
|
256
|
+
this.subscriptionHandlers.delete(prefixedChannel);
|
|
257
|
+
const interval = this.pollingIntervals.get(prefixedChannel);
|
|
258
|
+
if (interval) {
|
|
259
|
+
clearInterval(interval);
|
|
260
|
+
this.pollingIntervals.delete(prefixedChannel);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Publish using list-based approach (for polling subscribers).
|
|
268
|
+
* This is an alternative to native PUBLISH when subscribers are polling.
|
|
269
|
+
*/
|
|
270
|
+
async publishToQueue(channel, message) {
|
|
271
|
+
const prefixedChannel = this.prefixKey(`__pubsub__:${channel}`);
|
|
272
|
+
const listKey = `${prefixedChannel}:queue`;
|
|
273
|
+
await this.getConnectedClient().lpush(listKey, message);
|
|
274
|
+
}
|
|
275
|
+
getPubSubSuggestion() {
|
|
276
|
+
return "Enable pub/sub by setting enablePubSub: true in the adapter options.";
|
|
277
|
+
}
|
|
278
|
+
// ============================================
|
|
279
|
+
// Internal Helpers
|
|
280
|
+
// ============================================
|
|
281
|
+
/**
|
|
282
|
+
* Add prefix to a key.
|
|
283
|
+
*/
|
|
284
|
+
prefixKey(key) {
|
|
285
|
+
return this.keyPrefix + key;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Remove prefix from a key.
|
|
289
|
+
*/
|
|
290
|
+
unprefixKey(key) {
|
|
291
|
+
if (this.keyPrefix && key.startsWith(this.keyPrefix)) {
|
|
292
|
+
return key.slice(this.keyPrefix.length);
|
|
293
|
+
}
|
|
294
|
+
return key;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
export {
|
|
298
|
+
UpstashStorageAdapter
|
|
299
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/vercel-kv.ts
|
|
2
|
+
import { StorageConfigError, StorageConnectionError } from "../errors.mjs";
|
|
3
|
+
import { COMPARE_AND_DELETE_SCRIPT, COMPARE_AND_EXPIRE_SCRIPT } from "../utils/compare-and-delete.mjs";
|
|
4
|
+
import { validateTTL } from "../utils/ttl.mjs";
|
|
5
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
6
|
+
async function loadVercelKv() {
|
|
7
|
+
try {
|
|
8
|
+
const mod = await import("@vercel/kv");
|
|
9
|
+
const resolved = typeof mod.createClient === "function" ? mod : mod.default;
|
|
10
|
+
if (!resolved || typeof resolved.createClient !== "function") throw new Error("createClient missing");
|
|
11
|
+
return resolved;
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("@vercel/kv is required for Vercel KV storage adapter. Install it with: npm install @vercel/kv");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function createVercelKvClient(connection = {}) {
|
|
17
|
+
const url = connection.url ?? process.env["KV_REST_API_URL"];
|
|
18
|
+
const token = connection.token ?? process.env["KV_REST_API_TOKEN"];
|
|
19
|
+
if (!url || !token) {
|
|
20
|
+
throw new StorageConfigError("vercel-kv", "Pass url and token, or set KV_REST_API_URL and KV_REST_API_TOKEN.");
|
|
21
|
+
}
|
|
22
|
+
const { createClient } = await loadVercelKv();
|
|
23
|
+
return createClient({ url, token, cache: void 0, automaticDeserialization: false });
|
|
24
|
+
}
|
|
25
|
+
var VercelKvStorageAdapter = class extends BaseStorageAdapter {
|
|
26
|
+
backendName = "vercel-kv";
|
|
27
|
+
client;
|
|
28
|
+
options;
|
|
29
|
+
keyPrefix;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
super();
|
|
32
|
+
const url = options.url ?? process.env["KV_REST_API_URL"];
|
|
33
|
+
const token = options.token ?? process.env["KV_REST_API_TOKEN"];
|
|
34
|
+
if (!url || !token) {
|
|
35
|
+
throw new StorageConfigError(
|
|
36
|
+
"vercel-kv",
|
|
37
|
+
"KV_REST_API_URL and KV_REST_API_TOKEN must be provided via options or environment variables."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.options = { ...options, url, token };
|
|
41
|
+
this.keyPrefix = options.keyPrefix ?? "";
|
|
42
|
+
}
|
|
43
|
+
// ============================================
|
|
44
|
+
// Connection Lifecycle
|
|
45
|
+
// ============================================
|
|
46
|
+
async connect() {
|
|
47
|
+
if (this.connected) return;
|
|
48
|
+
try {
|
|
49
|
+
this.client = await createVercelKvClient({ url: this.options.url, token: this.options.token });
|
|
50
|
+
await this.client.exists("__healthcheck__");
|
|
51
|
+
this.connected = true;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
throw new StorageConnectionError(
|
|
54
|
+
"Failed to connect to Vercel KV",
|
|
55
|
+
e instanceof Error ? e : void 0,
|
|
56
|
+
"vercel-kv"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async disconnect() {
|
|
61
|
+
this.client = void 0;
|
|
62
|
+
this.connected = false;
|
|
63
|
+
}
|
|
64
|
+
async ping() {
|
|
65
|
+
if (!this.client) return false;
|
|
66
|
+
try {
|
|
67
|
+
await this.client.exists("__healthcheck__");
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ============================================
|
|
74
|
+
// Connection Helpers
|
|
75
|
+
// ============================================
|
|
76
|
+
/**
|
|
77
|
+
* Get the connected Vercel KV client, throwing if not connected.
|
|
78
|
+
*/
|
|
79
|
+
getConnectedClient() {
|
|
80
|
+
this.ensureConnected();
|
|
81
|
+
if (!this.client) {
|
|
82
|
+
throw new StorageConnectionError("Vercel KV client not connected", void 0, "vercel-kv");
|
|
83
|
+
}
|
|
84
|
+
return this.client;
|
|
85
|
+
}
|
|
86
|
+
// ============================================
|
|
87
|
+
// Core Operations
|
|
88
|
+
// ============================================
|
|
89
|
+
async get(key) {
|
|
90
|
+
return this.getConnectedClient().get(this.prefixKey(key));
|
|
91
|
+
}
|
|
92
|
+
async doSet(key, value, options) {
|
|
93
|
+
const client = this.getConnectedClient();
|
|
94
|
+
const prefixedKey = this.prefixKey(key);
|
|
95
|
+
const setOptions = {};
|
|
96
|
+
if (options?.ttlSeconds) {
|
|
97
|
+
setOptions.ex = options.ttlSeconds;
|
|
98
|
+
}
|
|
99
|
+
if (options?.ifNotExists) {
|
|
100
|
+
setOptions.nx = true;
|
|
101
|
+
} else if (options?.ifExists) {
|
|
102
|
+
setOptions.xx = true;
|
|
103
|
+
}
|
|
104
|
+
await client.set(prefixedKey, value, Object.keys(setOptions).length > 0 ? setOptions : void 0);
|
|
105
|
+
}
|
|
106
|
+
async delete(key) {
|
|
107
|
+
const result = await this.getConnectedClient().del(this.prefixKey(key));
|
|
108
|
+
return result > 0;
|
|
109
|
+
}
|
|
110
|
+
async deleteIfEquals(key, expectedValue) {
|
|
111
|
+
const deleted = await this.getConnectedClient().eval(
|
|
112
|
+
COMPARE_AND_DELETE_SCRIPT,
|
|
113
|
+
[this.prefixKey(key)],
|
|
114
|
+
[expectedValue]
|
|
115
|
+
);
|
|
116
|
+
return deleted === 1;
|
|
117
|
+
}
|
|
118
|
+
async expireIfEquals(key, expectedValue, ttlSeconds) {
|
|
119
|
+
validateTTL(ttlSeconds);
|
|
120
|
+
const extended = await this.getConnectedClient().eval(
|
|
121
|
+
COMPARE_AND_EXPIRE_SCRIPT,
|
|
122
|
+
[this.prefixKey(key)],
|
|
123
|
+
[expectedValue, String(ttlSeconds)]
|
|
124
|
+
);
|
|
125
|
+
return extended === 1;
|
|
126
|
+
}
|
|
127
|
+
async exists(key) {
|
|
128
|
+
const result = await this.getConnectedClient().exists(this.prefixKey(key));
|
|
129
|
+
return result > 0;
|
|
130
|
+
}
|
|
131
|
+
// ============================================
|
|
132
|
+
// Batch Operations
|
|
133
|
+
// ============================================
|
|
134
|
+
async mget(keys) {
|
|
135
|
+
if (keys.length === 0) return [];
|
|
136
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
137
|
+
return this.getConnectedClient().mget(...prefixedKeys);
|
|
138
|
+
}
|
|
139
|
+
async mdelete(keys) {
|
|
140
|
+
if (keys.length === 0) return 0;
|
|
141
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
142
|
+
return this.getConnectedClient().del(...prefixedKeys);
|
|
143
|
+
}
|
|
144
|
+
// ============================================
|
|
145
|
+
// TTL Operations
|
|
146
|
+
// ============================================
|
|
147
|
+
async expire(key, ttlSeconds) {
|
|
148
|
+
validateTTL(ttlSeconds);
|
|
149
|
+
const result = await this.getConnectedClient().expire(this.prefixKey(key), ttlSeconds);
|
|
150
|
+
return result === 1;
|
|
151
|
+
}
|
|
152
|
+
async ttl(key) {
|
|
153
|
+
const result = await this.getConnectedClient().ttl(this.prefixKey(key));
|
|
154
|
+
if (result === -2) return null;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
// ============================================
|
|
158
|
+
// Key Enumeration
|
|
159
|
+
// ============================================
|
|
160
|
+
async keys(pattern = "*") {
|
|
161
|
+
const client = this.getConnectedClient();
|
|
162
|
+
const prefixedPattern = this.prefixKey(pattern);
|
|
163
|
+
try {
|
|
164
|
+
const result = [];
|
|
165
|
+
let cursor = 0;
|
|
166
|
+
do {
|
|
167
|
+
const [nextCursor, keys] = await client.scan(cursor, {
|
|
168
|
+
match: prefixedPattern,
|
|
169
|
+
count: 100
|
|
170
|
+
});
|
|
171
|
+
const parsedCursor = typeof nextCursor === "string" ? parseInt(nextCursor, 10) : nextCursor;
|
|
172
|
+
cursor = Number.isNaN(parsedCursor) ? 0 : parsedCursor;
|
|
173
|
+
for (const key of keys) {
|
|
174
|
+
result.push(this.unprefixKey(key));
|
|
175
|
+
}
|
|
176
|
+
} while (cursor !== 0);
|
|
177
|
+
return result;
|
|
178
|
+
} catch {
|
|
179
|
+
const allKeys = await client.keys(prefixedPattern);
|
|
180
|
+
return allKeys.map((k) => this.unprefixKey(k));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ============================================
|
|
184
|
+
// Atomic Operations
|
|
185
|
+
// ============================================
|
|
186
|
+
async incr(key) {
|
|
187
|
+
return this.getConnectedClient().incr(this.prefixKey(key));
|
|
188
|
+
}
|
|
189
|
+
async decr(key) {
|
|
190
|
+
return this.getConnectedClient().decr(this.prefixKey(key));
|
|
191
|
+
}
|
|
192
|
+
async incrBy(key, amount) {
|
|
193
|
+
return this.getConnectedClient().incrby(this.prefixKey(key), amount);
|
|
194
|
+
}
|
|
195
|
+
// ============================================
|
|
196
|
+
// Pub/Sub (NOT SUPPORTED)
|
|
197
|
+
// ============================================
|
|
198
|
+
supportsPubSub() {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
getPubSubSuggestion() {
|
|
202
|
+
return "Vercel KV is REST-based and does not support pub/sub. Use Upstash adapter for pub/sub support.";
|
|
203
|
+
}
|
|
204
|
+
// ============================================
|
|
205
|
+
// Internal Helpers
|
|
206
|
+
// ============================================
|
|
207
|
+
/**
|
|
208
|
+
* Add prefix to a key.
|
|
209
|
+
*/
|
|
210
|
+
prefixKey(key) {
|
|
211
|
+
return this.keyPrefix + key;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Remove prefix from a key.
|
|
215
|
+
*/
|
|
216
|
+
unprefixKey(key) {
|
|
217
|
+
if (this.keyPrefix && key.startsWith(this.keyPrefix)) {
|
|
218
|
+
return key.slice(this.keyPrefix.length);
|
|
219
|
+
}
|
|
220
|
+
return key;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
export {
|
|
224
|
+
VercelKvStorageAdapter,
|
|
225
|
+
createVercelKvClient
|
|
226
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// libs/utils/src/storage/change-feed.ts
|
|
2
|
+
var DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS = 250;
|
|
3
|
+
var DEFAULT_CHANGE_FEED_COALESCE_MS = 50;
|
|
4
|
+
var TOPIC_PREFIX = "feed:";
|
|
5
|
+
var TopicFeed = class {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
options;
|
|
10
|
+
closed = false;
|
|
11
|
+
listeners = /* @__PURE__ */ new Map();
|
|
12
|
+
pending = /* @__PURE__ */ new Map();
|
|
13
|
+
subscribe(topic, listener) {
|
|
14
|
+
let topicListeners = this.listeners.get(topic);
|
|
15
|
+
if (!topicListeners) {
|
|
16
|
+
topicListeners = /* @__PURE__ */ new Set();
|
|
17
|
+
this.listeners.set(topic, topicListeners);
|
|
18
|
+
this.watch(topic);
|
|
19
|
+
}
|
|
20
|
+
const subscribed = topicListeners;
|
|
21
|
+
subscribed.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
if (!subscribed.delete(listener) || subscribed.size > 0 || this.listeners.get(topic) !== subscribed) return;
|
|
24
|
+
this.listeners.delete(topic);
|
|
25
|
+
this.unwatch(topic);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async close() {
|
|
29
|
+
this.closed = true;
|
|
30
|
+
for (const timer of this.pending.values()) clearTimeout(timer);
|
|
31
|
+
this.pending.clear();
|
|
32
|
+
for (const topic of this.listeners.keys()) this.unwatch(topic);
|
|
33
|
+
this.listeners.clear();
|
|
34
|
+
}
|
|
35
|
+
/** Tell the listeners of `topic` that it changed, once per coalescing window. */
|
|
36
|
+
changed(topic) {
|
|
37
|
+
if (this.closed || this.pending.has(topic) || !this.listeners.has(topic)) return;
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
this.pending.delete(topic);
|
|
40
|
+
for (const listener of [...this.listeners.get(topic) ?? []]) {
|
|
41
|
+
try {
|
|
42
|
+
listener();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.options.onError?.(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, this.options.coalesceMs ?? DEFAULT_CHANGE_FEED_COALESCE_MS);
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
this.pending.set(topic, timer);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var LocalChangeFeed = class extends TopicFeed {
|
|
53
|
+
kind = "local";
|
|
54
|
+
async publish(topic) {
|
|
55
|
+
this.changed(topic);
|
|
56
|
+
}
|
|
57
|
+
watch() {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
unwatch() {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var PubSubChangeFeed = class extends TopicFeed {
|
|
65
|
+
constructor(storage, options) {
|
|
66
|
+
super(options);
|
|
67
|
+
this.storage = storage;
|
|
68
|
+
this.stopReconnectListener = storage.onPubSubReconnect?.(() => {
|
|
69
|
+
for (const topic of this.channels.keys()) this.changed(topic);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
storage;
|
|
73
|
+
kind = "pubsub";
|
|
74
|
+
channels = /* @__PURE__ */ new Map();
|
|
75
|
+
stopReconnectListener;
|
|
76
|
+
async close() {
|
|
77
|
+
this.stopReconnectListener?.();
|
|
78
|
+
await super.close();
|
|
79
|
+
}
|
|
80
|
+
async publish(topic) {
|
|
81
|
+
await this.storage.publish(TOPIC_PREFIX + topic, "");
|
|
82
|
+
}
|
|
83
|
+
watch(topic) {
|
|
84
|
+
const subscription = this.storage.subscribe(TOPIC_PREFIX + topic, () => this.changed(topic)).catch((error) => {
|
|
85
|
+
this.options.onError?.(error);
|
|
86
|
+
return void 0;
|
|
87
|
+
});
|
|
88
|
+
this.channels.set(topic, subscription);
|
|
89
|
+
}
|
|
90
|
+
unwatch(topic) {
|
|
91
|
+
const subscription = this.channels.get(topic);
|
|
92
|
+
this.channels.delete(topic);
|
|
93
|
+
void subscription?.then((unsubscribe) => unsubscribe?.()).catch((error) => this.options.onError?.(error));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var pollingFeeds = /* @__PURE__ */ new Set();
|
|
97
|
+
var PollingChangeFeed = class extends TopicFeed {
|
|
98
|
+
constructor(storage, options) {
|
|
99
|
+
super(options);
|
|
100
|
+
this.storage = storage;
|
|
101
|
+
pollingFeeds.add(this);
|
|
102
|
+
}
|
|
103
|
+
storage;
|
|
104
|
+
kind = "poll";
|
|
105
|
+
/** The counter each followed topic had when last read; `undefined` until its first read. */
|
|
106
|
+
seen = /* @__PURE__ */ new Map();
|
|
107
|
+
timer;
|
|
108
|
+
polling = false;
|
|
109
|
+
lastVersion;
|
|
110
|
+
failing = false;
|
|
111
|
+
async publish(topic) {
|
|
112
|
+
const count = String(await this.storage.incr(TOPIC_PREFIX + topic));
|
|
113
|
+
if (this.seen.has(topic)) this.seen.set(topic, count);
|
|
114
|
+
const counterKey = this.storage.prefix + TOPIC_PREFIX + topic;
|
|
115
|
+
for (const feed of pollingFeeds) feed.published(counterKey);
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
pollingFeeds.delete(this);
|
|
119
|
+
await super.close();
|
|
120
|
+
clearTimeout(this.timer);
|
|
121
|
+
this.timer = void 0;
|
|
122
|
+
}
|
|
123
|
+
/** A feed of this process incremented `counterKey`. */
|
|
124
|
+
published(counterKey) {
|
|
125
|
+
const ownPrefix = this.storage.prefix + TOPIC_PREFIX;
|
|
126
|
+
if (!counterKey.startsWith(ownPrefix)) return;
|
|
127
|
+
const topic = counterKey.slice(ownPrefix.length);
|
|
128
|
+
if (this.seen.has(topic)) this.changed(topic);
|
|
129
|
+
}
|
|
130
|
+
watch(topic) {
|
|
131
|
+
this.seen.set(topic, void 0);
|
|
132
|
+
void this.storage.get(TOPIC_PREFIX + topic).then(
|
|
133
|
+
(count) => {
|
|
134
|
+
if (this.seen.has(topic) && this.seen.get(topic) === void 0) this.seen.set(topic, count);
|
|
135
|
+
},
|
|
136
|
+
(error) => this.options.onError?.(error)
|
|
137
|
+
);
|
|
138
|
+
this.schedule();
|
|
139
|
+
}
|
|
140
|
+
unwatch(topic) {
|
|
141
|
+
this.seen.delete(topic);
|
|
142
|
+
if (this.seen.size > 0) return;
|
|
143
|
+
clearTimeout(this.timer);
|
|
144
|
+
this.timer = void 0;
|
|
145
|
+
}
|
|
146
|
+
schedule() {
|
|
147
|
+
if (this.timer || this.polling || this.closed || this.seen.size === 0) return;
|
|
148
|
+
this.timer = setTimeout(() => {
|
|
149
|
+
this.timer = void 0;
|
|
150
|
+
void this.poll();
|
|
151
|
+
}, this.options.pollIntervalMs ?? DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS);
|
|
152
|
+
this.timer.unref?.();
|
|
153
|
+
}
|
|
154
|
+
async poll() {
|
|
155
|
+
this.polling = true;
|
|
156
|
+
try {
|
|
157
|
+
const version = await this.storage.root.dataVersion?.();
|
|
158
|
+
if (version !== this.lastVersion) {
|
|
159
|
+
const topics = [...this.seen.keys()];
|
|
160
|
+
const counts = await this.storage.mget(topics.map((topic) => TOPIC_PREFIX + topic));
|
|
161
|
+
this.lastVersion = version;
|
|
162
|
+
topics.forEach((topic, index) => this.compare(topic, counts[index]));
|
|
163
|
+
}
|
|
164
|
+
this.failing = false;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (!this.failing && !this.closed) this.options.onError?.(error);
|
|
167
|
+
this.failing = true;
|
|
168
|
+
} finally {
|
|
169
|
+
this.polling = false;
|
|
170
|
+
this.schedule();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
compare(topic, count) {
|
|
174
|
+
if (!this.seen.has(topic)) return;
|
|
175
|
+
const before = this.seen.get(topic);
|
|
176
|
+
this.seen.set(topic, count);
|
|
177
|
+
if (before !== void 0 && before !== count) this.changed(topic);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
function createChangeFeed(storage, options = {}) {
|
|
181
|
+
if (storage.supportsPubSub()) return new PubSubChangeFeed(storage, options);
|
|
182
|
+
if (storage.root.dataVersion) return new PollingChangeFeed(storage, options);
|
|
183
|
+
return new LocalChangeFeed(options);
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
DEFAULT_CHANGE_FEED_COALESCE_MS,
|
|
187
|
+
DEFAULT_CHANGE_FEED_POLL_INTERVAL_MS,
|
|
188
|
+
createChangeFeed
|
|
189
|
+
};
|