@frontmcp/utils 1.10.16 → 1.11.0-rc.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/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/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 +20 -4
- package/fs/index.d.ts +1 -1
- package/index.d.ts +9 -3
- 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,222 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/localstorage.ts
|
|
2
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
3
|
+
import { encryptAesGcm, decryptAesGcm, randomBytes, base64urlEncode, base64urlDecode } from "../../crypto/index.mjs";
|
|
4
|
+
import { matchesPattern } from "../utils/pattern.mjs";
|
|
5
|
+
var LocalStorageAdapter = class extends BaseStorageAdapter {
|
|
6
|
+
backendName = "localstorage";
|
|
7
|
+
prefix;
|
|
8
|
+
encryptionKey;
|
|
9
|
+
allowPlaintext;
|
|
10
|
+
encoder = new TextEncoder();
|
|
11
|
+
decoder = new TextDecoder();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super();
|
|
14
|
+
this.prefix = options?.prefix ?? "frontmcp:";
|
|
15
|
+
this.allowPlaintext = options?.allowPlaintext ?? true;
|
|
16
|
+
if (options?.encryptionKey) {
|
|
17
|
+
if (options.encryptionKey.length !== 32) {
|
|
18
|
+
throw new Error(`encryptionKey must be exactly 32 bytes, got ${options.encryptionKey.length}`);
|
|
19
|
+
}
|
|
20
|
+
this.encryptionKey = options.encryptionKey;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
assertAvailable() {
|
|
24
|
+
if (typeof localStorage === "undefined") {
|
|
25
|
+
throw new Error("localStorage is not available in this environment");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
key(k) {
|
|
29
|
+
return `${this.prefix}${k}`;
|
|
30
|
+
}
|
|
31
|
+
stripPrefix(fullKey) {
|
|
32
|
+
return fullKey.slice(this.prefix.length);
|
|
33
|
+
}
|
|
34
|
+
async connect() {
|
|
35
|
+
this.assertAvailable();
|
|
36
|
+
const probeKey = this.key("__probe__");
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(probeKey, "1");
|
|
39
|
+
const read = localStorage.getItem(probeKey);
|
|
40
|
+
if (read !== "1") {
|
|
41
|
+
throw new Error("localStorage probe: read-back mismatch");
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`localStorage is not writable: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
+
} finally {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(probeKey);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.connected = true;
|
|
52
|
+
}
|
|
53
|
+
async disconnect() {
|
|
54
|
+
this.connected = false;
|
|
55
|
+
}
|
|
56
|
+
async ping() {
|
|
57
|
+
const probeKey = this.key("__probe__");
|
|
58
|
+
try {
|
|
59
|
+
this.assertAvailable();
|
|
60
|
+
localStorage.setItem(probeKey, "1");
|
|
61
|
+
return localStorage.getItem(probeKey) === "1";
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
} finally {
|
|
65
|
+
try {
|
|
66
|
+
localStorage.removeItem(probeKey);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async get(key) {
|
|
72
|
+
const raw = localStorage.getItem(this.key(key));
|
|
73
|
+
if (raw === null) return null;
|
|
74
|
+
try {
|
|
75
|
+
const entry = JSON.parse(raw);
|
|
76
|
+
if (entry.e && entry.e < Date.now()) {
|
|
77
|
+
localStorage.removeItem(this.key(key));
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (entry._enc) {
|
|
81
|
+
if (!this.encryptionKey) return null;
|
|
82
|
+
const iv = base64urlDecode(entry._enc.iv);
|
|
83
|
+
const tag = base64urlDecode(entry._enc.tag);
|
|
84
|
+
const ciphertext = base64urlDecode(entry._enc.data);
|
|
85
|
+
const plainBytes = decryptAesGcm(this.encryptionKey, ciphertext, iv, tag);
|
|
86
|
+
return this.decoder.decode(plainBytes);
|
|
87
|
+
}
|
|
88
|
+
return entry.v;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async set(key, value, options) {
|
|
94
|
+
if (options?.ifNotExists) {
|
|
95
|
+
const existing = await this.get(key);
|
|
96
|
+
if (existing !== null) return;
|
|
97
|
+
}
|
|
98
|
+
if (options?.ifExists) {
|
|
99
|
+
const existing = await this.get(key);
|
|
100
|
+
if (existing === null) return;
|
|
101
|
+
}
|
|
102
|
+
await this.doSet(key, value, options);
|
|
103
|
+
}
|
|
104
|
+
async doSet(key, value, options) {
|
|
105
|
+
const entry = { v: "" };
|
|
106
|
+
if (options?.ttlSeconds) {
|
|
107
|
+
entry.e = Date.now() + options.ttlSeconds * 1e3;
|
|
108
|
+
}
|
|
109
|
+
if (this.encryptionKey) {
|
|
110
|
+
const iv = randomBytes(12);
|
|
111
|
+
const plainBytes = this.encoder.encode(value);
|
|
112
|
+
const { ciphertext, tag } = encryptAesGcm(this.encryptionKey, plainBytes, iv);
|
|
113
|
+
entry._enc = {
|
|
114
|
+
alg: "A256GCM",
|
|
115
|
+
iv: base64urlEncode(iv),
|
|
116
|
+
tag: base64urlEncode(tag),
|
|
117
|
+
data: base64urlEncode(ciphertext)
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
if (!this.allowPlaintext) {
|
|
121
|
+
throw new Error("Plaintext storage is disabled. Provide an encryptionKey or set allowPlaintext: true.");
|
|
122
|
+
}
|
|
123
|
+
entry.v = value;
|
|
124
|
+
}
|
|
125
|
+
localStorage.setItem(this.key(key), JSON.stringify(entry));
|
|
126
|
+
}
|
|
127
|
+
async delete(key) {
|
|
128
|
+
const existed = await this.get(key) !== null;
|
|
129
|
+
localStorage.removeItem(this.key(key));
|
|
130
|
+
return existed;
|
|
131
|
+
}
|
|
132
|
+
async exists(key) {
|
|
133
|
+
return await this.get(key) !== null;
|
|
134
|
+
}
|
|
135
|
+
async mget(keys) {
|
|
136
|
+
return Promise.all(keys.map((k) => this.get(k)));
|
|
137
|
+
}
|
|
138
|
+
async mset(entries) {
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
await this.set(entry.key, entry.value, entry.options);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async mdelete(keys) {
|
|
144
|
+
let count = 0;
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
if (await this.delete(key)) count++;
|
|
147
|
+
}
|
|
148
|
+
return count;
|
|
149
|
+
}
|
|
150
|
+
async expire(key, ttlSeconds) {
|
|
151
|
+
const value = await this.get(key);
|
|
152
|
+
if (value === null) return false;
|
|
153
|
+
await this.set(key, value, { ttlSeconds });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
async ttl(key) {
|
|
157
|
+
const raw = localStorage.getItem(this.key(key));
|
|
158
|
+
if (raw === null) return null;
|
|
159
|
+
try {
|
|
160
|
+
const entry = JSON.parse(raw);
|
|
161
|
+
if (!entry.e) return -1;
|
|
162
|
+
const remaining = Math.ceil((entry.e - Date.now()) / 1e3);
|
|
163
|
+
return remaining > 0 ? remaining : null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async keys(pattern) {
|
|
169
|
+
const result = [];
|
|
170
|
+
const candidates = [];
|
|
171
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
172
|
+
const fullKey = localStorage.key(i);
|
|
173
|
+
if (fullKey && fullKey.startsWith(this.prefix)) {
|
|
174
|
+
candidates.push(this.stripPrefix(fullKey));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const key of candidates) {
|
|
178
|
+
if (!pattern || pattern === "*" || this.matchPattern(key, pattern)) {
|
|
179
|
+
if (await this.get(key) !== null) {
|
|
180
|
+
result.push(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
async count(pattern) {
|
|
187
|
+
return (await this.keys(pattern)).length;
|
|
188
|
+
}
|
|
189
|
+
async incr(key) {
|
|
190
|
+
return this.incrBy(key, 1);
|
|
191
|
+
}
|
|
192
|
+
async decr(key) {
|
|
193
|
+
return this.incrBy(key, -1);
|
|
194
|
+
}
|
|
195
|
+
async incrBy(key, amount) {
|
|
196
|
+
if (!Number.isInteger(amount)) {
|
|
197
|
+
throw new Error(`amount must be an integer, got ${amount}`);
|
|
198
|
+
}
|
|
199
|
+
const current = await this.get(key);
|
|
200
|
+
if (current !== null && !/^-?\d+$/.test(current)) {
|
|
201
|
+
throw new Error(`Value at "${key}" is not an integer`);
|
|
202
|
+
}
|
|
203
|
+
const value = current !== null ? parseInt(current, 10) + amount : amount;
|
|
204
|
+
await this.set(key, String(value));
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
async publish() {
|
|
208
|
+
throw new Error("LocalStorage adapter does not support pub/sub");
|
|
209
|
+
}
|
|
210
|
+
async subscribe(_channel, _handler) {
|
|
211
|
+
throw new Error("LocalStorage adapter does not support pub/sub");
|
|
212
|
+
}
|
|
213
|
+
supportsPubSub() {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
matchPattern(key, pattern) {
|
|
217
|
+
return matchesPattern(key, pattern);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
export {
|
|
221
|
+
LocalStorageAdapter
|
|
222
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// libs/utils/src/storage/adapters/memory.ts
|
|
2
|
+
import { EventEmitter } from "../../event-emitter/browser-event-emitter.mjs";
|
|
3
|
+
import { StorageOperationError } from "../errors.mjs";
|
|
4
|
+
import { globToRegex } from "../utils/pattern.mjs";
|
|
5
|
+
import { expiresAtToTTL, isExpired, ttlToExpiresAt, validateTTL } from "../utils/ttl.mjs";
|
|
6
|
+
import { BaseStorageAdapter } from "./base.mjs";
|
|
7
|
+
var DEFAULT_SWEEP_INTERVAL_SECONDS = 60;
|
|
8
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
9
|
+
var MemoryStorageAdapter = class extends BaseStorageAdapter {
|
|
10
|
+
backendName = "memory";
|
|
11
|
+
store = /* @__PURE__ */ new Map();
|
|
12
|
+
emitter = new EventEmitter();
|
|
13
|
+
sweepInterval;
|
|
14
|
+
options;
|
|
15
|
+
// LRU tracking (simple linked list via insertion order in Map)
|
|
16
|
+
accessOrder = [];
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.options = {
|
|
20
|
+
enableSweeper: options.enableSweeper ?? true,
|
|
21
|
+
sweepIntervalSeconds: options.sweepIntervalSeconds ?? DEFAULT_SWEEP_INTERVAL_SECONDS,
|
|
22
|
+
maxEntries: options.maxEntries ?? 0
|
|
23
|
+
// 0 = unlimited
|
|
24
|
+
};
|
|
25
|
+
this.emitter.setMaxListeners(1e3);
|
|
26
|
+
}
|
|
27
|
+
// ============================================
|
|
28
|
+
// Connection Lifecycle
|
|
29
|
+
// ============================================
|
|
30
|
+
async connect() {
|
|
31
|
+
if (this.connected) return;
|
|
32
|
+
this.connected = true;
|
|
33
|
+
if (this.options.enableSweeper && this.options.sweepIntervalSeconds > 0) {
|
|
34
|
+
this.startSweeper();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async disconnect() {
|
|
38
|
+
if (!this.connected) return;
|
|
39
|
+
this.stopSweeper();
|
|
40
|
+
this.clearAllTimeouts();
|
|
41
|
+
this.store.clear();
|
|
42
|
+
this.accessOrder = [];
|
|
43
|
+
this.emitter.removeAllListeners();
|
|
44
|
+
this.connected = false;
|
|
45
|
+
}
|
|
46
|
+
async ping() {
|
|
47
|
+
return this.connected;
|
|
48
|
+
}
|
|
49
|
+
// ============================================
|
|
50
|
+
// Core Operations
|
|
51
|
+
// ============================================
|
|
52
|
+
async get(key) {
|
|
53
|
+
this.ensureConnected();
|
|
54
|
+
const entry = this.store.get(key);
|
|
55
|
+
if (!entry) return null;
|
|
56
|
+
if (isExpired(entry.expiresAt)) {
|
|
57
|
+
this.deleteEntry(key);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
this.touchLRU(key);
|
|
61
|
+
return entry.value;
|
|
62
|
+
}
|
|
63
|
+
async doSet(key, value, options) {
|
|
64
|
+
const existingEntry = this.store.get(key);
|
|
65
|
+
const exists = existingEntry !== void 0 && !isExpired(existingEntry.expiresAt);
|
|
66
|
+
if (options?.ifNotExists && exists) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (options?.ifExists && !exists) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.clearEntryTimeout(key);
|
|
73
|
+
const entry = { value };
|
|
74
|
+
if (options?.ttlSeconds) {
|
|
75
|
+
entry.expiresAt = ttlToExpiresAt(options.ttlSeconds);
|
|
76
|
+
const ttlMs = options.ttlSeconds * 1e3;
|
|
77
|
+
if (ttlMs < MAX_TIMEOUT_MS) {
|
|
78
|
+
entry.timeout = setTimeout(() => {
|
|
79
|
+
this.deleteEntry(key);
|
|
80
|
+
}, ttlMs);
|
|
81
|
+
if (entry.timeout.unref) {
|
|
82
|
+
entry.timeout.unref();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (this.options.maxEntries > 0 && !this.store.has(key)) {
|
|
87
|
+
while (this.store.size >= this.options.maxEntries) {
|
|
88
|
+
this.evictOldest();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.store.set(key, entry);
|
|
92
|
+
this.touchLRU(key);
|
|
93
|
+
}
|
|
94
|
+
async delete(key) {
|
|
95
|
+
this.ensureConnected();
|
|
96
|
+
return this.deleteEntry(key);
|
|
97
|
+
}
|
|
98
|
+
async deleteIfEquals(key, expectedValue) {
|
|
99
|
+
this.ensureConnected();
|
|
100
|
+
const entry = this.store.get(key);
|
|
101
|
+
if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
|
|
102
|
+
return this.deleteEntry(key);
|
|
103
|
+
}
|
|
104
|
+
async expireIfEquals(key, expectedValue, ttlSeconds) {
|
|
105
|
+
this.ensureConnected();
|
|
106
|
+
const entry = this.store.get(key);
|
|
107
|
+
if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
|
|
108
|
+
return this.expire(key, ttlSeconds);
|
|
109
|
+
}
|
|
110
|
+
async setIfEquals(key, expectedValue, value) {
|
|
111
|
+
this.ensureConnected();
|
|
112
|
+
const entry = this.store.get(key);
|
|
113
|
+
if (!entry || isExpired(entry.expiresAt) || entry.value !== expectedValue) return false;
|
|
114
|
+
await this.doSet(key, value);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
async exists(key) {
|
|
118
|
+
this.ensureConnected();
|
|
119
|
+
const entry = this.store.get(key);
|
|
120
|
+
if (!entry) return false;
|
|
121
|
+
if (isExpired(entry.expiresAt)) {
|
|
122
|
+
this.deleteEntry(key);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
// ============================================
|
|
128
|
+
// TTL Operations
|
|
129
|
+
// ============================================
|
|
130
|
+
async expire(key, ttlSeconds) {
|
|
131
|
+
this.ensureConnected();
|
|
132
|
+
validateTTL(ttlSeconds);
|
|
133
|
+
const entry = this.store.get(key);
|
|
134
|
+
if (!entry || isExpired(entry.expiresAt)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
this.clearEntryTimeout(key);
|
|
138
|
+
entry.expiresAt = ttlToExpiresAt(ttlSeconds);
|
|
139
|
+
const ttlMs = ttlSeconds * 1e3;
|
|
140
|
+
if (ttlMs < MAX_TIMEOUT_MS) {
|
|
141
|
+
entry.timeout = setTimeout(() => {
|
|
142
|
+
this.deleteEntry(key);
|
|
143
|
+
}, ttlMs);
|
|
144
|
+
if (entry.timeout.unref) {
|
|
145
|
+
entry.timeout.unref();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
async ttl(key) {
|
|
151
|
+
this.ensureConnected();
|
|
152
|
+
const entry = this.store.get(key);
|
|
153
|
+
if (!entry) return null;
|
|
154
|
+
if (isExpired(entry.expiresAt)) {
|
|
155
|
+
this.deleteEntry(key);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (entry.expiresAt === void 0) {
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
return expiresAtToTTL(entry.expiresAt);
|
|
162
|
+
}
|
|
163
|
+
// ============================================
|
|
164
|
+
// Key Enumeration
|
|
165
|
+
// ============================================
|
|
166
|
+
async keys(pattern = "*") {
|
|
167
|
+
this.ensureConnected();
|
|
168
|
+
const regex = globToRegex(pattern);
|
|
169
|
+
const result = [];
|
|
170
|
+
for (const [key, entry] of this.store) {
|
|
171
|
+
if (isExpired(entry.expiresAt)) {
|
|
172
|
+
this.deleteEntry(key);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (regex.test(key)) {
|
|
176
|
+
result.push(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
// ============================================
|
|
182
|
+
// Atomic Operations
|
|
183
|
+
// ============================================
|
|
184
|
+
async incr(key) {
|
|
185
|
+
return this.incrBy(key, 1);
|
|
186
|
+
}
|
|
187
|
+
async decr(key) {
|
|
188
|
+
return this.incrBy(key, -1);
|
|
189
|
+
}
|
|
190
|
+
async incrBy(key, amount) {
|
|
191
|
+
this.ensureConnected();
|
|
192
|
+
const entry = this.store.get(key);
|
|
193
|
+
let currentValue = 0;
|
|
194
|
+
if (entry && !isExpired(entry.expiresAt)) {
|
|
195
|
+
const parsed = parseInt(entry.value, 10);
|
|
196
|
+
if (isNaN(parsed)) {
|
|
197
|
+
throw new StorageOperationError("incrBy", key, "Value is not an integer");
|
|
198
|
+
}
|
|
199
|
+
currentValue = parsed;
|
|
200
|
+
}
|
|
201
|
+
const newValue = currentValue + amount;
|
|
202
|
+
const newEntry = { value: String(newValue) };
|
|
203
|
+
if (entry?.expiresAt && !isExpired(entry.expiresAt)) {
|
|
204
|
+
newEntry.expiresAt = entry.expiresAt;
|
|
205
|
+
const remainingMs = entry.expiresAt - Date.now();
|
|
206
|
+
if (remainingMs > 0 && remainingMs < MAX_TIMEOUT_MS) {
|
|
207
|
+
this.clearEntryTimeout(key);
|
|
208
|
+
newEntry.timeout = setTimeout(() => {
|
|
209
|
+
this.deleteEntry(key);
|
|
210
|
+
}, remainingMs);
|
|
211
|
+
if (newEntry.timeout.unref) {
|
|
212
|
+
newEntry.timeout.unref();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.store.set(key, newEntry);
|
|
217
|
+
this.touchLRU(key);
|
|
218
|
+
return newValue;
|
|
219
|
+
}
|
|
220
|
+
// ============================================
|
|
221
|
+
// Pub/Sub
|
|
222
|
+
// ============================================
|
|
223
|
+
supportsPubSub() {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
async publish(channel, message) {
|
|
227
|
+
this.ensureConnected();
|
|
228
|
+
return this.emitter.listenerCount(channel) > 0 ? (this.emitter.emit(channel, message, channel), this.emitter.listenerCount(channel)) : 0;
|
|
229
|
+
}
|
|
230
|
+
async subscribe(channel, handler) {
|
|
231
|
+
this.ensureConnected();
|
|
232
|
+
const wrappedHandler = (message, ch) => {
|
|
233
|
+
handler(message, ch);
|
|
234
|
+
};
|
|
235
|
+
this.emitter.on(channel, wrappedHandler);
|
|
236
|
+
return async () => {
|
|
237
|
+
this.emitter.off(channel, wrappedHandler);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// ============================================
|
|
241
|
+
// Internal Helpers
|
|
242
|
+
// ============================================
|
|
243
|
+
/**
|
|
244
|
+
* Delete an entry and clear its timeout.
|
|
245
|
+
*/
|
|
246
|
+
deleteEntry(key) {
|
|
247
|
+
const entry = this.store.get(key);
|
|
248
|
+
if (!entry) return false;
|
|
249
|
+
this.clearEntryTimeout(key);
|
|
250
|
+
this.store.delete(key);
|
|
251
|
+
this.removeLRU(key);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Clear timeout for an entry.
|
|
256
|
+
*/
|
|
257
|
+
clearEntryTimeout(key) {
|
|
258
|
+
const entry = this.store.get(key);
|
|
259
|
+
if (entry?.timeout) {
|
|
260
|
+
clearTimeout(entry.timeout);
|
|
261
|
+
entry.timeout = void 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Clear all timeouts.
|
|
266
|
+
*/
|
|
267
|
+
clearAllTimeouts() {
|
|
268
|
+
for (const [key] of this.store) {
|
|
269
|
+
this.clearEntryTimeout(key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Update LRU access order.
|
|
274
|
+
*/
|
|
275
|
+
touchLRU(key) {
|
|
276
|
+
if (this.options.maxEntries <= 0) return;
|
|
277
|
+
const idx = this.accessOrder.indexOf(key);
|
|
278
|
+
if (idx !== -1) {
|
|
279
|
+
this.accessOrder.splice(idx, 1);
|
|
280
|
+
}
|
|
281
|
+
this.accessOrder.push(key);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Remove key from LRU tracking.
|
|
285
|
+
*/
|
|
286
|
+
removeLRU(key) {
|
|
287
|
+
if (this.options.maxEntries <= 0) return;
|
|
288
|
+
const idx = this.accessOrder.indexOf(key);
|
|
289
|
+
if (idx !== -1) {
|
|
290
|
+
this.accessOrder.splice(idx, 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Evict oldest entry (LRU).
|
|
295
|
+
*/
|
|
296
|
+
evictOldest() {
|
|
297
|
+
if (this.accessOrder.length === 0) return;
|
|
298
|
+
const oldestKey = this.accessOrder.shift();
|
|
299
|
+
if (oldestKey) {
|
|
300
|
+
this.deleteEntry(oldestKey);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Start background sweeper.
|
|
305
|
+
*/
|
|
306
|
+
startSweeper() {
|
|
307
|
+
this.sweepInterval = setInterval(() => {
|
|
308
|
+
this.sweep();
|
|
309
|
+
}, this.options.sweepIntervalSeconds * 1e3);
|
|
310
|
+
if (this.sweepInterval.unref) {
|
|
311
|
+
this.sweepInterval.unref();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Stop background sweeper.
|
|
316
|
+
*/
|
|
317
|
+
stopSweeper() {
|
|
318
|
+
if (this.sweepInterval) {
|
|
319
|
+
clearInterval(this.sweepInterval);
|
|
320
|
+
this.sweepInterval = void 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Sweep expired entries.
|
|
325
|
+
*/
|
|
326
|
+
sweep() {
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
for (const [key, entry] of this.store) {
|
|
329
|
+
if (entry.expiresAt && now >= entry.expiresAt) {
|
|
330
|
+
this.deleteEntry(key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// ============================================
|
|
335
|
+
// Stats (for debugging)
|
|
336
|
+
// ============================================
|
|
337
|
+
/**
|
|
338
|
+
* Get storage statistics.
|
|
339
|
+
*/
|
|
340
|
+
getStats() {
|
|
341
|
+
return {
|
|
342
|
+
size: this.store.size,
|
|
343
|
+
maxEntries: this.options.maxEntries,
|
|
344
|
+
sweeperActive: this.sweepInterval !== void 0
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
export {
|
|
349
|
+
MemoryStorageAdapter
|
|
350
|
+
};
|