@orkify/cli 1.0.0-beta.5 → 1.0.0-beta.6
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 +10 -5
- package/package.json +8 -31
- package/packages/cache/README.md +0 -114
- package/packages/cache/dist/CacheClient.d.ts +0 -26
- package/packages/cache/dist/CacheClient.d.ts.map +0 -1
- package/packages/cache/dist/CacheClient.js +0 -174
- package/packages/cache/dist/CacheClient.js.map +0 -1
- package/packages/cache/dist/CacheFileStore.d.ts +0 -45
- package/packages/cache/dist/CacheFileStore.d.ts.map +0 -1
- package/packages/cache/dist/CacheFileStore.js +0 -446
- package/packages/cache/dist/CacheFileStore.js.map +0 -1
- package/packages/cache/dist/CachePersistence.d.ts +0 -9
- package/packages/cache/dist/CachePersistence.d.ts.map +0 -1
- package/packages/cache/dist/CachePersistence.js +0 -67
- package/packages/cache/dist/CachePersistence.js.map +0 -1
- package/packages/cache/dist/CachePrimary.d.ts +0 -25
- package/packages/cache/dist/CachePrimary.d.ts.map +0 -1
- package/packages/cache/dist/CachePrimary.js +0 -155
- package/packages/cache/dist/CachePrimary.js.map +0 -1
- package/packages/cache/dist/CacheStore.d.ts +0 -50
- package/packages/cache/dist/CacheStore.d.ts.map +0 -1
- package/packages/cache/dist/CacheStore.js +0 -271
- package/packages/cache/dist/CacheStore.js.map +0 -1
- package/packages/cache/dist/constants.d.ts +0 -6
- package/packages/cache/dist/constants.d.ts.map +0 -1
- package/packages/cache/dist/constants.js +0 -9
- package/packages/cache/dist/constants.js.map +0 -1
- package/packages/cache/dist/index.d.ts +0 -16
- package/packages/cache/dist/index.d.ts.map +0 -1
- package/packages/cache/dist/index.js +0 -86
- package/packages/cache/dist/index.js.map +0 -1
- package/packages/cache/dist/serialize.d.ts +0 -9
- package/packages/cache/dist/serialize.d.ts.map +0 -1
- package/packages/cache/dist/serialize.js +0 -40
- package/packages/cache/dist/serialize.js.map +0 -1
- package/packages/cache/dist/types.d.ts +0 -123
- package/packages/cache/dist/types.d.ts.map +0 -1
- package/packages/cache/dist/types.js +0 -2
- package/packages/cache/dist/types.js.map +0 -1
- package/packages/cache/package.json +0 -27
- package/packages/cache/src/CacheClient.ts +0 -227
- package/packages/cache/src/CacheFileStore.ts +0 -528
- package/packages/cache/src/CachePersistence.ts +0 -89
- package/packages/cache/src/CachePrimary.ts +0 -172
- package/packages/cache/src/CacheStore.ts +0 -308
- package/packages/cache/src/constants.ts +0 -10
- package/packages/cache/src/index.ts +0 -100
- package/packages/cache/src/serialize.ts +0 -49
- package/packages/cache/src/types.ts +0 -156
- package/packages/cache/tsconfig.json +0 -18
- package/packages/cache/tsconfig.tsbuildinfo +0 -1
- package/packages/next/README.md +0 -166
- package/packages/next/dist/error-capture.d.ts +0 -34
- package/packages/next/dist/error-capture.d.ts.map +0 -1
- package/packages/next/dist/error-capture.js +0 -130
- package/packages/next/dist/error-capture.js.map +0 -1
- package/packages/next/dist/error-handler.d.ts +0 -10
- package/packages/next/dist/error-handler.d.ts.map +0 -1
- package/packages/next/dist/error-handler.js +0 -186
- package/packages/next/dist/error-handler.js.map +0 -1
- package/packages/next/dist/isr-cache.d.ts +0 -9
- package/packages/next/dist/isr-cache.d.ts.map +0 -1
- package/packages/next/dist/isr-cache.js +0 -86
- package/packages/next/dist/isr-cache.js.map +0 -1
- package/packages/next/dist/stream.d.ts +0 -5
- package/packages/next/dist/stream.d.ts.map +0 -1
- package/packages/next/dist/stream.js +0 -22
- package/packages/next/dist/stream.js.map +0 -1
- package/packages/next/dist/types.d.ts +0 -33
- package/packages/next/dist/types.d.ts.map +0 -1
- package/packages/next/dist/types.js +0 -6
- package/packages/next/dist/types.js.map +0 -1
- package/packages/next/dist/use-cache.d.ts +0 -4
- package/packages/next/dist/use-cache.d.ts.map +0 -1
- package/packages/next/dist/use-cache.js +0 -86
- package/packages/next/dist/use-cache.js.map +0 -1
- package/packages/next/dist/utils.d.ts +0 -32
- package/packages/next/dist/utils.d.ts.map +0 -1
- package/packages/next/dist/utils.js +0 -88
- package/packages/next/dist/utils.js.map +0 -1
- package/packages/next/package.json +0 -52
- package/packages/next/src/error-capture.ts +0 -177
- package/packages/next/src/error-handler.ts +0 -221
- package/packages/next/src/isr-cache.ts +0 -100
- package/packages/next/src/stream.ts +0 -23
- package/packages/next/src/types.ts +0 -33
- package/packages/next/src/use-cache.ts +0 -99
- package/packages/next/src/utils.ts +0 -102
- package/packages/next/tsconfig.json +0 -19
- package/packages/next/tsconfig.tsbuildinfo +0 -1
|
@@ -1,528 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import type { CacheConfig, CacheEntry, CacheSnapshot, CacheStats, ICacheStore } from './types.js';
|
|
6
|
-
import { CACHE_CLEANUP_INTERVAL } from './constants.js';
|
|
7
|
-
import { CacheStore } from './CacheStore.js';
|
|
8
|
-
import { deserialize, serialize, type Serialized, serializedByteLength } from './serialize.js';
|
|
9
|
-
|
|
10
|
-
/** Metadata stored in the disk index (no values — those are in individual files) */
|
|
11
|
-
interface DiskMeta {
|
|
12
|
-
expiresAt?: number;
|
|
13
|
-
file: string; // sha256.json
|
|
14
|
-
tags?: string[];
|
|
15
|
-
timestamp: number; // epoch ms when entry was written to disk
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** On-disk entry file format */
|
|
19
|
-
interface DiskEntry {
|
|
20
|
-
expiresAt?: number;
|
|
21
|
-
key: string;
|
|
22
|
-
tags?: string[];
|
|
23
|
-
timestamp: number; // epoch ms when entry was written to disk
|
|
24
|
-
value: Serialized;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** On-disk index format */
|
|
28
|
-
interface DiskIndex {
|
|
29
|
-
entries: Record<string, DiskMeta>;
|
|
30
|
-
tagTimestamps: Array<[string, number]>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class CacheFileStore implements ICacheStore {
|
|
34
|
-
private cacheDir: string;
|
|
35
|
-
private diskIndex = new Map<string, DiskMeta>();
|
|
36
|
-
private diskSweepTimer: ReturnType<typeof setInterval> | undefined;
|
|
37
|
-
private diskTagIndex = new Map<string, Set<string>>(); // tag → disk keys
|
|
38
|
-
private entriesDir: string;
|
|
39
|
-
private indexPath: string;
|
|
40
|
-
private indexDirty = false;
|
|
41
|
-
private loadIndexPromise: Promise<void> | undefined;
|
|
42
|
-
private persistPromise: Promise<void> | undefined;
|
|
43
|
-
private readOnly: boolean;
|
|
44
|
-
private store: CacheStore;
|
|
45
|
-
private tagTimestamps = new Map<string, number>(); // disk-only tag timestamps
|
|
46
|
-
|
|
47
|
-
constructor(processName: string, config?: CacheConfig, options?: { readOnly?: boolean }) {
|
|
48
|
-
this.readOnly = options?.readOnly ?? false;
|
|
49
|
-
this.cacheDir = join(
|
|
50
|
-
process.env.HOME ?? process.env.USERPROFILE ?? '.',
|
|
51
|
-
'.orkify',
|
|
52
|
-
'cache',
|
|
53
|
-
processName
|
|
54
|
-
);
|
|
55
|
-
this.entriesDir = join(this.cacheDir, 'entries');
|
|
56
|
-
this.indexPath = join(this.cacheDir, 'index.json');
|
|
57
|
-
|
|
58
|
-
this.store = new CacheStore(
|
|
59
|
-
config,
|
|
60
|
-
this.readOnly
|
|
61
|
-
? undefined
|
|
62
|
-
: (key, entry, reason) => {
|
|
63
|
-
if (reason === 'lru') {
|
|
64
|
-
// Spill evicted entry to disk (fire-and-forget)
|
|
65
|
-
void this.writeToDisk(key, entry);
|
|
66
|
-
} else if (reason === 'expired') {
|
|
67
|
-
// Clean expired entry from disk
|
|
68
|
-
void this.deleteFromDisk(key);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (!this.readOnly) {
|
|
74
|
-
// Periodic disk sweep for expired entries
|
|
75
|
-
this.diskSweepTimer = setInterval(() => void this.sweepDisk(), CACHE_CLEANUP_INTERVAL);
|
|
76
|
-
this.diskSweepTimer.unref();
|
|
77
|
-
|
|
78
|
-
// Load disk index from any previous session (entries promoted lazily via getAsync).
|
|
79
|
-
// Store the promise so getAsync can await it before checking the index.
|
|
80
|
-
this.loadIndexPromise = this.loadIndex();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --- ICacheStore public API ---
|
|
85
|
-
|
|
86
|
-
get<T>(key: string): T | undefined {
|
|
87
|
-
return this.store.get<T>(key);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async getAsync<T>(key: string): Promise<T | undefined> {
|
|
91
|
-
// Hot path: sync memory lookup
|
|
92
|
-
const memValue = this.store.get<T>(key);
|
|
93
|
-
if (memValue !== undefined) return memValue;
|
|
94
|
-
|
|
95
|
-
// Ensure disk index is loaded from any previous session before checking it
|
|
96
|
-
if (this.loadIndexPromise) {
|
|
97
|
-
await this.loadIndexPromise;
|
|
98
|
-
this.loadIndexPromise = undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Cold path: check disk index (populated in full mode, empty in readOnly mode)
|
|
102
|
-
const meta = this.diskIndex.get(key);
|
|
103
|
-
|
|
104
|
-
// In full mode, if not in index it's a true miss.
|
|
105
|
-
// In readOnly mode, diskIndex is empty — always try file directly.
|
|
106
|
-
if (!meta && !this.readOnly) return undefined;
|
|
107
|
-
|
|
108
|
-
// Compute file path: from index if available, otherwise derive from key hash
|
|
109
|
-
const fileName = meta?.file ?? createHash('sha256').update(key).digest('hex') + '.json';
|
|
110
|
-
const filePath = join(this.entriesDir, fileName);
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const content = await readFile(filePath, 'utf-8');
|
|
114
|
-
const disk: DiskEntry = JSON.parse(content);
|
|
115
|
-
|
|
116
|
-
// Check TTL expiration
|
|
117
|
-
if (disk.expiresAt !== undefined && disk.expiresAt < Date.now()) {
|
|
118
|
-
if (!this.readOnly) void this.deleteFromDisk(key);
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check tag timestamps — if any tag was invalidated after the entry was written, it's stale
|
|
123
|
-
if (disk.tags && disk.tags.length > 0) {
|
|
124
|
-
const tagExp = this.getTagExpiration(disk.tags);
|
|
125
|
-
if (tagExp > disk.timestamp) {
|
|
126
|
-
if (!this.readOnly) void this.deleteFromDisk(key);
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Deserialize the value
|
|
132
|
-
const value = deserialize(disk.value) as T;
|
|
133
|
-
|
|
134
|
-
// Promote to memory (may evict other entries to disk via callback in full mode;
|
|
135
|
-
// in readOnly mode evictions just drop since there's no onEvict)
|
|
136
|
-
this.store.set(key, value, disk.expiresAt, disk.tags);
|
|
137
|
-
|
|
138
|
-
// In full mode, remove from disk index (it's now in memory)
|
|
139
|
-
if (meta && !this.readOnly) {
|
|
140
|
-
this.removeDiskMeta(key);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return value;
|
|
144
|
-
} catch {
|
|
145
|
-
// File read/parse failed — remove stale index entry in full mode
|
|
146
|
-
if (meta && !this.readOnly) {
|
|
147
|
-
this.removeDiskMeta(key);
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
set(
|
|
154
|
-
key: string,
|
|
155
|
-
value: unknown,
|
|
156
|
-
expiresAt?: number,
|
|
157
|
-
tags?: string[],
|
|
158
|
-
precomputedByteSize?: number
|
|
159
|
-
): void {
|
|
160
|
-
// Remove from disk if it exists there (we're overwriting)
|
|
161
|
-
if (this.diskIndex.has(key)) {
|
|
162
|
-
void this.deleteFromDisk(key);
|
|
163
|
-
}
|
|
164
|
-
this.store.set(key, value, expiresAt, tags, precomputedByteSize);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
delete(key: string): boolean {
|
|
168
|
-
const memDeleted = this.store.delete(key);
|
|
169
|
-
const diskHad = this.diskIndex.has(key);
|
|
170
|
-
if (diskHad) {
|
|
171
|
-
void this.deleteFromDisk(key);
|
|
172
|
-
}
|
|
173
|
-
return memDeleted || diskHad;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
clear(): void {
|
|
177
|
-
this.store.clear();
|
|
178
|
-
// Clear all disk entries
|
|
179
|
-
const diskKeys = [...this.diskIndex.keys()];
|
|
180
|
-
for (const key of diskKeys) {
|
|
181
|
-
void this.deleteFromDisk(key);
|
|
182
|
-
}
|
|
183
|
-
this.diskIndex.clear();
|
|
184
|
-
this.diskTagIndex.clear();
|
|
185
|
-
this.tagTimestamps.clear();
|
|
186
|
-
this.indexDirty = true;
|
|
187
|
-
void this.persistIndex();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
has(key: string): boolean {
|
|
191
|
-
if (this.store.has(key)) return true;
|
|
192
|
-
const meta = this.diskIndex.get(key);
|
|
193
|
-
if (!meta) return false;
|
|
194
|
-
if (meta.expiresAt !== undefined && meta.expiresAt < Date.now()) {
|
|
195
|
-
if (!this.readOnly) void this.deleteFromDisk(key);
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
stats(): CacheStats {
|
|
202
|
-
const base = this.store.stats();
|
|
203
|
-
return { ...base, diskSize: this.diskIndex.size };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
invalidateTag(tag: string): string[] {
|
|
207
|
-
// Invalidate in memory
|
|
208
|
-
const deleted = this.store.invalidateTag(tag);
|
|
209
|
-
|
|
210
|
-
// Record tag timestamp for disk entries
|
|
211
|
-
this.tagTimestamps.set(tag, Date.now());
|
|
212
|
-
|
|
213
|
-
// Delete disk entries with this tag
|
|
214
|
-
const diskKeys = this.diskTagIndex.get(tag);
|
|
215
|
-
if (diskKeys && diskKeys.size > 0) {
|
|
216
|
-
for (const key of [...diskKeys]) {
|
|
217
|
-
void this.deleteFromDisk(key);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return deleted;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
getTagExpiration(tags: string[]): number {
|
|
225
|
-
// Check both in-memory store and disk-level tag timestamps
|
|
226
|
-
const memExp = this.store.getTagExpiration(tags);
|
|
227
|
-
let diskExp = 0;
|
|
228
|
-
for (const tag of tags) {
|
|
229
|
-
const ts = this.tagTimestamps.get(tag);
|
|
230
|
-
if (ts !== undefined && ts > diskExp) {
|
|
231
|
-
diskExp = ts;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return Math.max(memExp, diskExp);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
applyTagTimestamp(tag: string, timestamp: number): void {
|
|
238
|
-
this.store.applyTagTimestamp(tag, timestamp);
|
|
239
|
-
this.tagTimestamps.set(tag, timestamp);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
applySet(key: string, value: unknown, expiresAt?: number, tags?: string[]): void {
|
|
243
|
-
if (this.diskIndex.has(key)) {
|
|
244
|
-
void this.deleteFromDisk(key);
|
|
245
|
-
}
|
|
246
|
-
this.store.applySet(key, value, expiresAt, tags);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
applyDelete(key: string): void {
|
|
250
|
-
this.store.applyDelete(key);
|
|
251
|
-
if (this.diskIndex.has(key)) {
|
|
252
|
-
void this.deleteFromDisk(key);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
applySnapshot(snapshot: CacheSnapshot): void {
|
|
257
|
-
this.store.applySnapshot(snapshot);
|
|
258
|
-
// Snapshot replaces everything — clear disk too
|
|
259
|
-
for (const key of [...this.diskIndex.keys()]) {
|
|
260
|
-
void this.deleteFromDisk(key);
|
|
261
|
-
}
|
|
262
|
-
this.diskIndex.clear();
|
|
263
|
-
this.diskTagIndex.clear();
|
|
264
|
-
// Merge tag timestamps from snapshot
|
|
265
|
-
this.tagTimestamps.clear();
|
|
266
|
-
for (const [tag, ts] of snapshot.tagTimestamps) {
|
|
267
|
-
this.tagTimestamps.set(tag, ts);
|
|
268
|
-
}
|
|
269
|
-
this.indexDirty = true;
|
|
270
|
-
void this.persistIndex();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
serialize(): CacheSnapshot {
|
|
274
|
-
return this.store.serialize();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
destroy(): void {
|
|
278
|
-
if (this.diskSweepTimer) {
|
|
279
|
-
clearInterval(this.diskSweepTimer);
|
|
280
|
-
this.diskSweepTimer = undefined;
|
|
281
|
-
}
|
|
282
|
-
this.store.destroy();
|
|
283
|
-
this.diskIndex.clear();
|
|
284
|
-
this.diskTagIndex.clear();
|
|
285
|
-
this.tagTimestamps.clear();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// --- File-backed specific methods ---
|
|
289
|
-
|
|
290
|
-
/** Flush all in-memory entries to disk (called on graceful shutdown). No-op in readOnly mode. */
|
|
291
|
-
async flush(): Promise<void> {
|
|
292
|
-
if (this.readOnly) return;
|
|
293
|
-
const snapshot = this.store.serialize();
|
|
294
|
-
for (const [key, entry] of snapshot.entries) {
|
|
295
|
-
const byteSize = serializedByteLength(serialize(entry.value));
|
|
296
|
-
await this.writeToDisk(key, {
|
|
297
|
-
byteSize,
|
|
298
|
-
value: entry.value,
|
|
299
|
-
expiresAt: entry.expiresAt,
|
|
300
|
-
lastAccessedAt: Date.now(),
|
|
301
|
-
tags: entry.tags,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
// Force a final index write — fire-and-forget calls from writeToDisk may have
|
|
305
|
-
// already persisted a partial index and cleared the dirty flag
|
|
306
|
-
this.indexDirty = true;
|
|
307
|
-
await this.persistIndex();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Synchronous flush for use in process 'exit' handlers where async I/O is unavailable. */
|
|
311
|
-
flushSync(): void {
|
|
312
|
-
if (this.readOnly) return;
|
|
313
|
-
const snapshot = this.store.serialize();
|
|
314
|
-
if (snapshot.entries.length === 0) return;
|
|
315
|
-
|
|
316
|
-
mkdirSync(this.entriesDir, { recursive: true });
|
|
317
|
-
|
|
318
|
-
for (const [key, entry] of snapshot.entries) {
|
|
319
|
-
const fileName = createHash('sha256').update(key).digest('hex') + '.json';
|
|
320
|
-
const filePath = join(this.entriesDir, fileName);
|
|
321
|
-
const now = Date.now();
|
|
322
|
-
const disk: DiskEntry = {
|
|
323
|
-
key,
|
|
324
|
-
value: serialize(entry.value),
|
|
325
|
-
expiresAt: entry.expiresAt,
|
|
326
|
-
tags: entry.tags,
|
|
327
|
-
timestamp: now,
|
|
328
|
-
};
|
|
329
|
-
writeFileSync(filePath, JSON.stringify(disk), 'utf-8');
|
|
330
|
-
|
|
331
|
-
const meta: DiskMeta = { file: fileName, expiresAt: entry.expiresAt, timestamp: now };
|
|
332
|
-
if (entry.tags && entry.tags.length > 0) meta.tags = entry.tags;
|
|
333
|
-
this.diskIndex.set(key, meta);
|
|
334
|
-
if (entry.tags) {
|
|
335
|
-
for (const tag of entry.tags) {
|
|
336
|
-
let keys = this.diskTagIndex.get(tag);
|
|
337
|
-
if (!keys) {
|
|
338
|
-
keys = new Set();
|
|
339
|
-
this.diskTagIndex.set(tag, keys);
|
|
340
|
-
}
|
|
341
|
-
keys.add(key);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
mkdirSync(this.cacheDir, { recursive: true });
|
|
347
|
-
const data: DiskIndex = {
|
|
348
|
-
entries: Object.fromEntries(this.diskIndex),
|
|
349
|
-
tagTimestamps: [...this.tagTimestamps],
|
|
350
|
-
};
|
|
351
|
-
writeFileSync(this.indexPath, JSON.stringify(data), 'utf-8');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** Load disk index on startup (values are loaded lazily on access). No-op in readOnly mode. */
|
|
355
|
-
async loadIndex(): Promise<void> {
|
|
356
|
-
if (this.readOnly) return;
|
|
357
|
-
if (!existsSync(this.indexPath)) return;
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
const content = await readFile(this.indexPath, 'utf-8');
|
|
361
|
-
const data: DiskIndex = JSON.parse(content);
|
|
362
|
-
|
|
363
|
-
this.diskIndex.clear();
|
|
364
|
-
this.diskTagIndex.clear();
|
|
365
|
-
|
|
366
|
-
const now = Date.now();
|
|
367
|
-
for (const [key, meta] of Object.entries(data.entries)) {
|
|
368
|
-
// Skip expired entries
|
|
369
|
-
if (meta.expiresAt !== undefined && meta.expiresAt < now) continue;
|
|
370
|
-
// Ensure timestamp exists (older indexes may lack it)
|
|
371
|
-
if (!meta.timestamp) meta.timestamp = now;
|
|
372
|
-
this.diskIndex.set(key, meta);
|
|
373
|
-
if (meta.tags) {
|
|
374
|
-
for (const tag of meta.tags) {
|
|
375
|
-
let keys = this.diskTagIndex.get(tag);
|
|
376
|
-
if (!keys) {
|
|
377
|
-
keys = new Set();
|
|
378
|
-
this.diskTagIndex.set(tag, keys);
|
|
379
|
-
}
|
|
380
|
-
keys.add(key);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Restore tag timestamps
|
|
386
|
-
if (data.tagTimestamps) {
|
|
387
|
-
for (const [tag, ts] of data.tagTimestamps) {
|
|
388
|
-
this.tagTimestamps.set(tag, ts);
|
|
389
|
-
this.store.applyTagTimestamp(tag, ts);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
} catch {
|
|
393
|
-
// Corrupted index — start fresh
|
|
394
|
-
this.diskIndex.clear();
|
|
395
|
-
this.diskTagIndex.clear();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// --- Private helpers ---
|
|
400
|
-
|
|
401
|
-
private async writeToDisk(key: string, entry: CacheEntry): Promise<void> {
|
|
402
|
-
try {
|
|
403
|
-
await mkdir(this.entriesDir, { recursive: true });
|
|
404
|
-
|
|
405
|
-
const fileName = createHash('sha256').update(key).digest('hex') + '.json';
|
|
406
|
-
const filePath = join(this.entriesDir, fileName);
|
|
407
|
-
|
|
408
|
-
const now = Date.now();
|
|
409
|
-
const disk: DiskEntry = {
|
|
410
|
-
key,
|
|
411
|
-
value: serialize(entry.value),
|
|
412
|
-
expiresAt: entry.expiresAt,
|
|
413
|
-
tags: entry.tags,
|
|
414
|
-
timestamp: now,
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Atomic write
|
|
418
|
-
const tmpPath = filePath + '.tmp';
|
|
419
|
-
await writeFile(tmpPath, JSON.stringify(disk), 'utf-8');
|
|
420
|
-
await rename(tmpPath, filePath);
|
|
421
|
-
|
|
422
|
-
// Update disk index
|
|
423
|
-
const meta: DiskMeta = { file: fileName, expiresAt: entry.expiresAt, timestamp: now };
|
|
424
|
-
if (entry.tags && entry.tags.length > 0) {
|
|
425
|
-
meta.tags = entry.tags;
|
|
426
|
-
}
|
|
427
|
-
this.diskIndex.set(key, meta);
|
|
428
|
-
|
|
429
|
-
// Update disk tag index
|
|
430
|
-
if (entry.tags) {
|
|
431
|
-
for (const tag of entry.tags) {
|
|
432
|
-
let keys = this.diskTagIndex.get(tag);
|
|
433
|
-
if (!keys) {
|
|
434
|
-
keys = new Set();
|
|
435
|
-
this.diskTagIndex.set(tag, keys);
|
|
436
|
-
}
|
|
437
|
-
keys.add(key);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
this.indexDirty = true;
|
|
442
|
-
void this.persistIndex();
|
|
443
|
-
} catch {
|
|
444
|
-
// Disk write failed — entry stays in-memory only
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private async deleteFromDisk(key: string): Promise<void> {
|
|
449
|
-
const meta = this.diskIndex.get(key);
|
|
450
|
-
if (!meta) return;
|
|
451
|
-
|
|
452
|
-
this.removeDiskMeta(key);
|
|
453
|
-
|
|
454
|
-
try {
|
|
455
|
-
const filePath = join(this.entriesDir, meta.file);
|
|
456
|
-
if (existsSync(filePath)) {
|
|
457
|
-
await unlink(filePath);
|
|
458
|
-
}
|
|
459
|
-
} catch {
|
|
460
|
-
// File already gone or inaccessible — ok
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
this.indexDirty = true;
|
|
464
|
-
void this.persistIndex();
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private removeDiskMeta(key: string): void {
|
|
468
|
-
const meta = this.diskIndex.get(key);
|
|
469
|
-
if (!meta) return;
|
|
470
|
-
|
|
471
|
-
// Remove from disk tag index
|
|
472
|
-
if (meta.tags) {
|
|
473
|
-
for (const tag of meta.tags) {
|
|
474
|
-
const keys = this.diskTagIndex.get(tag);
|
|
475
|
-
if (keys) {
|
|
476
|
-
keys.delete(key);
|
|
477
|
-
if (keys.size === 0) {
|
|
478
|
-
this.diskTagIndex.delete(tag);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
this.diskIndex.delete(key);
|
|
485
|
-
this.indexDirty = true;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private async sweepDisk(): Promise<void> {
|
|
489
|
-
const now = Date.now();
|
|
490
|
-
for (const [key, meta] of [...this.diskIndex]) {
|
|
491
|
-
if (meta.expiresAt !== undefined && meta.expiresAt < now) {
|
|
492
|
-
await this.deleteFromDisk(key);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
private async persistIndex(): Promise<void> {
|
|
498
|
-
if (this.readOnly || !this.indexDirty) return;
|
|
499
|
-
this.indexDirty = false;
|
|
500
|
-
|
|
501
|
-
const doWrite = async (): Promise<void> => {
|
|
502
|
-
try {
|
|
503
|
-
await mkdir(this.cacheDir, { recursive: true });
|
|
504
|
-
|
|
505
|
-
const data: DiskIndex = {
|
|
506
|
-
entries: Object.fromEntries(this.diskIndex),
|
|
507
|
-
tagTimestamps: [...this.tagTimestamps],
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const tmpPath = this.indexPath + '.tmp';
|
|
511
|
-
await writeFile(tmpPath, JSON.stringify(data), 'utf-8');
|
|
512
|
-
await rename(tmpPath, this.indexPath);
|
|
513
|
-
} catch {
|
|
514
|
-
// Index write failed — will retry on next change
|
|
515
|
-
this.indexDirty = true;
|
|
516
|
-
}
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
// Chain onto any in-flight write so they don't overlap
|
|
520
|
-
this.persistPromise = (this.persistPromise ?? Promise.resolve()).then(doWrite);
|
|
521
|
-
await this.persistPromise;
|
|
522
|
-
|
|
523
|
-
// If new changes arrived during the write, persist again
|
|
524
|
-
if (this.indexDirty) {
|
|
525
|
-
void this.persistIndex();
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import type { CacheSnapshot, SerializedCacheEntry } from './types.js';
|
|
5
|
-
import { CACHE_DIR } from './constants.js';
|
|
6
|
-
import { deserialize, serialize, type Serialized } from './serialize.js';
|
|
7
|
-
|
|
8
|
-
/** On-disk format — values are encoded as { data, encoding } */
|
|
9
|
-
interface DiskEntry {
|
|
10
|
-
expiresAt?: number;
|
|
11
|
-
tags?: string[];
|
|
12
|
-
value: Serialized;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface DiskSnapshot {
|
|
16
|
-
entries: Array<[string, DiskEntry]>;
|
|
17
|
-
tagTimestamps: Array<[string, number]>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class CachePersistence {
|
|
21
|
-
private filePath: string;
|
|
22
|
-
|
|
23
|
-
constructor(processName: string) {
|
|
24
|
-
this.filePath = join(CACHE_DIR, `${processName}.json`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async save(snapshot: CacheSnapshot): Promise<void> {
|
|
28
|
-
const dir = dirname(this.filePath);
|
|
29
|
-
if (!existsSync(dir)) {
|
|
30
|
-
await mkdir(dir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const diskEntries: Array<[string, DiskEntry]> = snapshot.entries.map(([key, entry]) => {
|
|
34
|
-
const disk: DiskEntry = {
|
|
35
|
-
value: serialize(entry.value),
|
|
36
|
-
expiresAt: entry.expiresAt,
|
|
37
|
-
};
|
|
38
|
-
if (entry.tags && entry.tags.length > 0) {
|
|
39
|
-
disk.tags = entry.tags;
|
|
40
|
-
}
|
|
41
|
-
return [key, disk];
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const diskData: DiskSnapshot = { entries: diskEntries, tagTimestamps: snapshot.tagTimestamps };
|
|
45
|
-
|
|
46
|
-
// Atomic write: temp file → rename
|
|
47
|
-
const tmpPath = this.filePath + '.tmp';
|
|
48
|
-
await writeFile(tmpPath, JSON.stringify(diskData), 'utf-8');
|
|
49
|
-
await rename(tmpPath, this.filePath);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async load(): Promise<CacheSnapshot> {
|
|
53
|
-
if (!existsSync(this.filePath)) {
|
|
54
|
-
return { entries: [], tagTimestamps: [] };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const content = await readFile(this.filePath, 'utf-8');
|
|
59
|
-
const raw: DiskSnapshot = JSON.parse(content);
|
|
60
|
-
const diskEntries = raw.entries;
|
|
61
|
-
const tagTimestamps = raw.tagTimestamps;
|
|
62
|
-
|
|
63
|
-
const now = Date.now();
|
|
64
|
-
const entries: Array<[string, SerializedCacheEntry]> = diskEntries
|
|
65
|
-
.filter(([, entry]) => entry.expiresAt === undefined || entry.expiresAt > now)
|
|
66
|
-
.map(([key, entry]) => {
|
|
67
|
-
const result: SerializedCacheEntry = {
|
|
68
|
-
value: deserialize(entry.value),
|
|
69
|
-
expiresAt: entry.expiresAt,
|
|
70
|
-
};
|
|
71
|
-
if (entry.tags) {
|
|
72
|
-
result.tags = entry.tags;
|
|
73
|
-
}
|
|
74
|
-
return [key, result] as [string, SerializedCacheEntry];
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return { entries, tagTimestamps };
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.warn(`[orkify:cache] Failed to load cache from ${this.filePath}:`, err);
|
|
80
|
-
return { entries: [], tagTimestamps: [] };
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async clear(): Promise<void> {
|
|
85
|
-
if (existsSync(this.filePath)) {
|
|
86
|
-
await unlink(this.filePath);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|