aegis-bridge 2.7.0 → 2.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/dist/memory-bridge.d.ts +27 -0
- package/dist/memory-bridge.js +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
interface MemoryEntry {
|
|
2
|
+
value: string;
|
|
3
|
+
namespace: string;
|
|
4
|
+
key: string;
|
|
5
|
+
created_at: number;
|
|
6
|
+
updated_at: number;
|
|
7
|
+
expires_at?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class MemoryBridge {
|
|
10
|
+
private reaperIntervalMs;
|
|
11
|
+
private store;
|
|
12
|
+
private persistPath;
|
|
13
|
+
private reaperTimer;
|
|
14
|
+
private saveTimer;
|
|
15
|
+
constructor(persistPath?: string | null, reaperIntervalMs?: number);
|
|
16
|
+
set(key: string, value: string, ttlSeconds?: number): MemoryEntry;
|
|
17
|
+
get(key: string): MemoryEntry | null;
|
|
18
|
+
delete(key: string): boolean;
|
|
19
|
+
list(prefix?: string): MemoryEntry[];
|
|
20
|
+
resolveKeys(keys: string[]): Map<string, string>;
|
|
21
|
+
load(): Promise<void>;
|
|
22
|
+
save(): Promise<void>;
|
|
23
|
+
private scheduleSave;
|
|
24
|
+
startReaper(): void;
|
|
25
|
+
stopReaper(): void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, renameSync, writeFileSync, readFileSync } from "fs";
|
|
2
|
+
const KEY_REGEX = /^(.+?)\/(.+)$/;
|
|
3
|
+
const MAX_KEY_LEN = 256;
|
|
4
|
+
const MAX_VALUE_SIZE = 100 * 1024; // 100KB
|
|
5
|
+
export class MemoryBridge {
|
|
6
|
+
reaperIntervalMs;
|
|
7
|
+
store = new Map();
|
|
8
|
+
persistPath = null;
|
|
9
|
+
reaperTimer = null;
|
|
10
|
+
saveTimer = null;
|
|
11
|
+
constructor(persistPath = null, reaperIntervalMs = 60_000) {
|
|
12
|
+
this.reaperIntervalMs = reaperIntervalMs;
|
|
13
|
+
this.persistPath = persistPath;
|
|
14
|
+
}
|
|
15
|
+
set(key, value, ttlSeconds) {
|
|
16
|
+
if (value.length > MAX_VALUE_SIZE)
|
|
17
|
+
throw new Error("Value exceeds maximum size");
|
|
18
|
+
const m = KEY_REGEX.exec(key);
|
|
19
|
+
if (!m)
|
|
20
|
+
throw new Error(`Invalid key format: must be namespace/key, got "${key}"`);
|
|
21
|
+
const [, namespace, keyName] = m;
|
|
22
|
+
if (key.length > MAX_KEY_LEN)
|
|
23
|
+
throw new Error("Key exceeds maximum length");
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const entry = {
|
|
26
|
+
value, namespace, key,
|
|
27
|
+
created_at: this.store.has(key) ? this.store.get(key).created_at : now,
|
|
28
|
+
updated_at: now,
|
|
29
|
+
expires_at: ttlSeconds ? now + ttlSeconds * 1000 : undefined,
|
|
30
|
+
};
|
|
31
|
+
this.store.set(key, entry);
|
|
32
|
+
this.scheduleSave();
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
get(key) {
|
|
36
|
+
const entry = this.store.get(key);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return null;
|
|
39
|
+
if (entry.expires_at && Date.now() > entry.expires_at) {
|
|
40
|
+
this.store.delete(key);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
delete(key) {
|
|
46
|
+
const deleted = this.store.delete(key);
|
|
47
|
+
if (deleted)
|
|
48
|
+
this.scheduleSave();
|
|
49
|
+
return deleted;
|
|
50
|
+
}
|
|
51
|
+
list(prefix) {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const entries = [...this.store.values()].filter(e => !e.expires_at || now <= e.expires_at);
|
|
54
|
+
if (!prefix)
|
|
55
|
+
return entries;
|
|
56
|
+
return entries.filter(e => e.key.startsWith(prefix));
|
|
57
|
+
}
|
|
58
|
+
resolveKeys(keys) {
|
|
59
|
+
const result = new Map();
|
|
60
|
+
for (const k of keys) {
|
|
61
|
+
const e = this.get(k);
|
|
62
|
+
if (e)
|
|
63
|
+
result.set(k, e.value);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
async load() {
|
|
68
|
+
if (!this.persistPath || !existsSync(this.persistPath))
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
const raw = JSON.parse(readFileSync(this.persistPath, "utf-8"));
|
|
72
|
+
if (Array.isArray(raw)) {
|
|
73
|
+
for (const e of raw) {
|
|
74
|
+
if (e.key && e.value)
|
|
75
|
+
this.store.set(e.key, e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore corrupt file */ }
|
|
80
|
+
}
|
|
81
|
+
async save() {
|
|
82
|
+
if (!this.persistPath)
|
|
83
|
+
return;
|
|
84
|
+
const entries = [...this.store.values()];
|
|
85
|
+
const tmp = this.persistPath + ".tmp";
|
|
86
|
+
writeFileSync(tmp, JSON.stringify(entries, null, 2));
|
|
87
|
+
renameSync(tmp, this.persistPath);
|
|
88
|
+
}
|
|
89
|
+
scheduleSave() {
|
|
90
|
+
if (this.saveTimer)
|
|
91
|
+
return;
|
|
92
|
+
this.saveTimer = setTimeout(async () => {
|
|
93
|
+
this.saveTimer = null;
|
|
94
|
+
await this.save();
|
|
95
|
+
}, 1000);
|
|
96
|
+
}
|
|
97
|
+
startReaper() {
|
|
98
|
+
if (this.reaperTimer)
|
|
99
|
+
return;
|
|
100
|
+
this.reaperTimer = setInterval(() => {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
for (const [k, e] of this.store) {
|
|
103
|
+
if (e.expires_at && now > e.expires_at)
|
|
104
|
+
this.store.delete(k);
|
|
105
|
+
}
|
|
106
|
+
}, this.reaperIntervalMs);
|
|
107
|
+
}
|
|
108
|
+
stopReaper() {
|
|
109
|
+
if (this.reaperTimer) {
|
|
110
|
+
clearInterval(this.reaperTimer);
|
|
111
|
+
this.reaperTimer = null;
|
|
112
|
+
}
|
|
113
|
+
if (this.saveTimer) {
|
|
114
|
+
clearTimeout(this.saveTimer);
|
|
115
|
+
this.saveTimer = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|