@dynlabs/react-native-immutable-file-cache 1.0.0-alpha.2 → 1.0.0-alpha.3
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/lib/commonjs/adapters/memoryAdapter.js +1 -0
- package/lib/commonjs/adapters/memoryAdapter.js.map +1 -1
- package/lib/commonjs/adapters/rnfsAdapter.js +9 -4
- package/lib/commonjs/adapters/rnfsAdapter.js.map +1 -1
- package/lib/commonjs/adapters/webAdapter.js +1 -0
- package/lib/commonjs/adapters/webAdapter.js.map +1 -1
- package/lib/commonjs/core/adapter.js +54 -0
- package/lib/commonjs/core/adapter.js.map +1 -1
- package/lib/commonjs/core/cacheEngine.js +452 -59
- package/lib/commonjs/core/cacheEngine.js.map +1 -1
- package/lib/commonjs/core/errors.js +9 -6
- package/lib/commonjs/core/errors.js.map +1 -1
- package/lib/commonjs/core/hash.js +3 -3
- package/lib/commonjs/core/hash.js.map +1 -1
- package/lib/commonjs/core/indexStore.js +85 -8
- package/lib/commonjs/core/indexStore.js.map +1 -1
- package/lib/commonjs/core/prune.js +42 -11
- package/lib/commonjs/core/prune.js.map +1 -1
- package/lib/commonjs/core/types.js +132 -0
- package/lib/commonjs/core/types.js.map +1 -1
- package/lib/commonjs/index.js +33 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/adapters/memoryAdapter.js +1 -0
- package/lib/module/adapters/memoryAdapter.js.map +1 -1
- package/lib/module/adapters/rnfsAdapter.js +9 -4
- package/lib/module/adapters/rnfsAdapter.js.map +1 -1
- package/lib/module/adapters/webAdapter.js +1 -0
- package/lib/module/adapters/webAdapter.js.map +1 -1
- package/lib/module/core/adapter.js +48 -0
- package/lib/module/core/adapter.js.map +1 -1
- package/lib/module/core/cacheEngine.js +453 -60
- package/lib/module/core/cacheEngine.js.map +1 -1
- package/lib/module/core/errors.js +9 -6
- package/lib/module/core/errors.js.map +1 -1
- package/lib/module/core/hash.js +3 -3
- package/lib/module/core/hash.js.map +1 -1
- package/lib/module/core/indexStore.js +86 -8
- package/lib/module/core/indexStore.js.map +1 -1
- package/lib/module/core/prune.js +40 -11
- package/lib/module/core/prune.js.map +1 -1
- package/lib/module/core/types.js +130 -1
- package/lib/module/core/types.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/adapters/memoryAdapter.d.ts.map +1 -1
- package/lib/typescript/src/adapters/rnfsAdapter.d.ts.map +1 -1
- package/lib/typescript/src/adapters/webAdapter.d.ts.map +1 -1
- package/lib/typescript/src/core/adapter.d.ts +16 -0
- package/lib/typescript/src/core/adapter.d.ts.map +1 -1
- package/lib/typescript/src/core/cacheEngine.d.ts +120 -1
- package/lib/typescript/src/core/cacheEngine.d.ts.map +1 -1
- package/lib/typescript/src/core/errors.d.ts +6 -5
- package/lib/typescript/src/core/errors.d.ts.map +1 -1
- package/lib/typescript/src/core/indexStore.d.ts +7 -0
- package/lib/typescript/src/core/indexStore.d.ts.map +1 -1
- package/lib/typescript/src/core/prune.d.ts +22 -8
- package/lib/typescript/src/core/prune.d.ts.map +1 -1
- package/lib/typescript/src/core/types.d.ts +153 -0
- package/lib/typescript/src/core/types.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/memoryAdapter.ts +3 -0
- package/src/adapters/rnfsAdapter.ts +11 -4
- package/src/adapters/webAdapter.ts +1 -0
- package/src/core/adapter.ts +28 -0
- package/src/core/cacheEngine.ts +476 -62
- package/src/core/errors.ts +8 -6
- package/src/core/hash.ts +3 -3
- package/src/core/indexStore.ts +99 -11
- package/src/core/prune.ts +44 -14
- package/src/core/types.ts +194 -0
- package/src/index.ts +22 -0
package/src/core/errors.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { TBinarySource } from "./adapter";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Base error class for cache operations.
|
|
5
|
+
* Supports ES2022 Error.cause for proper error chaining.
|
|
5
6
|
*/
|
|
6
7
|
export abstract class CacheError extends Error {
|
|
7
8
|
abstract readonly code: string;
|
|
8
9
|
|
|
9
|
-
constructor(message: string) {
|
|
10
|
-
super(message);
|
|
10
|
+
constructor(message: string, options?: { cause?: Error }) {
|
|
11
|
+
super(message, options);
|
|
11
12
|
this.name = this.constructor.name;
|
|
12
13
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
14
|
}
|
|
@@ -36,10 +37,11 @@ export class AdapterIOError extends CacheError {
|
|
|
36
37
|
constructor(
|
|
37
38
|
public readonly operation: string,
|
|
38
39
|
public readonly path: string,
|
|
39
|
-
|
|
40
|
+
cause?: Error
|
|
40
41
|
) {
|
|
41
42
|
super(
|
|
42
|
-
`Adapter I/O error during "${operation}" at path "${path}": ${cause?.message ?? "unknown"}
|
|
43
|
+
`Adapter I/O error during "${operation}" at path "${path}": ${cause?.message ?? "unknown"}`,
|
|
44
|
+
cause ? { cause } : undefined
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -52,9 +54,9 @@ export class CorruptIndexError extends CacheError {
|
|
|
52
54
|
|
|
53
55
|
constructor(
|
|
54
56
|
public readonly reason: string,
|
|
55
|
-
|
|
57
|
+
cause?: Error
|
|
56
58
|
) {
|
|
57
|
-
super(`Cache index is corrupt: ${reason}
|
|
59
|
+
super(`Cache index is corrupt: ${reason}`, cause ? { cause } : undefined);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
package/src/core/hash.ts
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
function arrayBufferToHex(buffer: ArrayBuffer): string {
|
|
10
10
|
const bytes = new Uint8Array(buffer);
|
|
11
|
-
|
|
11
|
+
const hexParts: string[] = new Array<string>(bytes.length);
|
|
12
12
|
for (let i = 0; i < bytes.length; i++) {
|
|
13
|
-
|
|
13
|
+
hexParts[i] = bytes[i].toString(16).padStart(2, "0");
|
|
14
14
|
}
|
|
15
|
-
return
|
|
15
|
+
return hexParts.join("");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
package/src/core/indexStore.ts
CHANGED
|
@@ -3,7 +3,42 @@ import type { ICacheIndex, ICacheEntryMeta } from "./types";
|
|
|
3
3
|
import { CorruptIndexError, AdapterIOError } from "./errors";
|
|
4
4
|
import { createEmptyIndex } from "./prune";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Current index schema version.
|
|
8
|
+
* Increment when making breaking changes to ICacheIndex structure.
|
|
9
|
+
*/
|
|
10
|
+
const CURRENT_INDEX_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimum supported version for migration.
|
|
14
|
+
* Versions below this will be rebuilt from scratch.
|
|
15
|
+
*/
|
|
16
|
+
const MIN_SUPPORTED_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Migration function type.
|
|
20
|
+
* Takes an index at version N and returns an index at version N+1.
|
|
21
|
+
*/
|
|
22
|
+
type TIndexMigrator = (index: unknown) => unknown;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registry of migration functions.
|
|
26
|
+
* Key is the source version, value migrates to source version + 1.
|
|
27
|
+
*
|
|
28
|
+
* Example: When upgrading from v1 to v2, add:
|
|
29
|
+
* ```
|
|
30
|
+
* 1: (index) => {
|
|
31
|
+
* // Transform v1 index to v2 format
|
|
32
|
+
* return { ...index, version: 2, newField: defaultValue };
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
const INDEX_MIGRATIONS: Record<number, TIndexMigrator> = {
|
|
37
|
+
// No migrations yet - v1 is the initial version
|
|
38
|
+
// Future migrations will be added here:
|
|
39
|
+
// 1: (index) => migrateV1ToV2(index),
|
|
40
|
+
// 2: (index) => migrateV2ToV3(index),
|
|
41
|
+
};
|
|
7
42
|
|
|
8
43
|
/**
|
|
9
44
|
* Manages persistence of the cache index via the storage adapter.
|
|
@@ -101,7 +136,7 @@ export class IndexStore {
|
|
|
101
136
|
}
|
|
102
137
|
|
|
103
138
|
const rebuiltIndex: ICacheIndex = {
|
|
104
|
-
version:
|
|
139
|
+
version: CURRENT_INDEX_VERSION,
|
|
105
140
|
entries,
|
|
106
141
|
totalSizeBytes,
|
|
107
142
|
lastModifiedAt: Date.now(),
|
|
@@ -114,18 +149,71 @@ export class IndexStore {
|
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
/**
|
|
117
|
-
*
|
|
152
|
+
* Applies migrations to upgrade an index from an older version to current.
|
|
153
|
+
* @param parsed - The parsed index object (may be any version)
|
|
154
|
+
* @returns The migrated index at current version
|
|
155
|
+
* @throws CorruptIndexError if version is unsupported or migration fails
|
|
118
156
|
*/
|
|
119
|
-
private
|
|
157
|
+
private _migrateIndex(parsed: unknown): unknown {
|
|
120
158
|
if (typeof parsed !== "object" || parsed === null) {
|
|
121
159
|
throw new CorruptIndexError("Index is not an object");
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
const obj = parsed as Record<string, unknown>;
|
|
163
|
+
let version = obj.version;
|
|
164
|
+
|
|
165
|
+
// Validate version is a number
|
|
166
|
+
if (typeof version !== "number") {
|
|
167
|
+
throw new CorruptIndexError(`Invalid version type: ${typeof version}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if version is too old to migrate
|
|
171
|
+
if (version < MIN_SUPPORTED_VERSION) {
|
|
172
|
+
throw new CorruptIndexError(
|
|
173
|
+
`Version ${version} is too old. Minimum supported: ${MIN_SUPPORTED_VERSION}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if version is from the future
|
|
178
|
+
if (version > CURRENT_INDEX_VERSION) {
|
|
179
|
+
throw new CorruptIndexError(
|
|
180
|
+
`Version ${version} is newer than current (${CURRENT_INDEX_VERSION}). ` +
|
|
181
|
+
`Please upgrade the package.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Apply migrations sequentially
|
|
186
|
+
let migrated: unknown = parsed;
|
|
187
|
+
while (version < CURRENT_INDEX_VERSION) {
|
|
188
|
+
const migrator = INDEX_MIGRATIONS[version];
|
|
189
|
+
if (!migrator) {
|
|
190
|
+
throw new CorruptIndexError(`No migration path from version ${version} to ${version + 1}`);
|
|
191
|
+
}
|
|
192
|
+
migrated = migrator(migrated);
|
|
193
|
+
version++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return migrated;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Validates that the parsed object is a valid ICacheIndex.
|
|
201
|
+
*/
|
|
202
|
+
private _validateIndex(parsed: unknown): ICacheIndex {
|
|
203
|
+
// First, migrate to current version if needed
|
|
204
|
+
const migrated = this._migrateIndex(parsed);
|
|
205
|
+
|
|
206
|
+
if (typeof migrated !== "object" || migrated === null) {
|
|
207
|
+
throw new CorruptIndexError("Index is not an object");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const obj = migrated as Record<string, unknown>;
|
|
125
211
|
|
|
126
|
-
//
|
|
127
|
-
if (obj.version !==
|
|
128
|
-
throw new CorruptIndexError(
|
|
212
|
+
// Verify version is current after migration
|
|
213
|
+
if (obj.version !== CURRENT_INDEX_VERSION) {
|
|
214
|
+
throw new CorruptIndexError(
|
|
215
|
+
`Migration failed: expected version ${CURRENT_INDEX_VERSION}, got ${String(obj.version)}`
|
|
216
|
+
);
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
// Check entries
|
|
@@ -150,7 +238,7 @@ export class IndexStore {
|
|
|
150
238
|
}
|
|
151
239
|
|
|
152
240
|
return {
|
|
153
|
-
version:
|
|
241
|
+
version: CURRENT_INDEX_VERSION,
|
|
154
242
|
entries: entries as Record<string, ICacheEntryMeta>,
|
|
155
243
|
totalSizeBytes: obj.totalSizeBytes,
|
|
156
244
|
lastModifiedAt: obj.lastModifiedAt,
|
|
@@ -160,14 +248,14 @@ export class IndexStore {
|
|
|
160
248
|
/**
|
|
161
249
|
* Validates that an entry has all required fields.
|
|
162
250
|
*/
|
|
163
|
-
private _validateEntry(key: string, entry: unknown):
|
|
251
|
+
private _validateEntry(key: string, entry: unknown): asserts entry is ICacheEntryMeta {
|
|
164
252
|
if (typeof entry !== "object" || entry === null) {
|
|
165
253
|
throw new CorruptIndexError(`Entry "${key}" is not an object`);
|
|
166
254
|
}
|
|
167
255
|
|
|
168
256
|
const obj = entry as Record<string, unknown>;
|
|
169
|
-
const requiredStrings = ["key", "hash", "ext"];
|
|
170
|
-
const requiredNumbers = ["sizeBytes", "createdAt", "lastAccessedAt"];
|
|
257
|
+
const requiredStrings = ["key", "hash", "ext"] as const;
|
|
258
|
+
const requiredNumbers = ["sizeBytes", "createdAt", "lastAccessedAt"] as const;
|
|
171
259
|
|
|
172
260
|
for (const field of requiredStrings) {
|
|
173
261
|
if (typeof obj[field] !== "string") {
|
package/src/core/prune.ts
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type { ICacheIndex, ICacheEntryMeta } from "./types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Current index schema version.
|
|
5
|
+
* Keep in sync with indexStore.ts CURRENT_INDEX_VERSION.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const INDEX_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a cache entry has expired.
|
|
11
|
+
* @param entry - The cache entry to check
|
|
12
|
+
* @param now - Current timestamp in milliseconds (defaults to Date.now())
|
|
13
|
+
* @returns true if the entry has expired, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export function isEntryExpired(entry: ICacheEntryMeta, now: number = Date.now()): boolean {
|
|
16
|
+
return entry.expiresAt !== undefined && entry.expiresAt < now;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if a cache entry is valid (not expired).
|
|
21
|
+
* @param entry - The cache entry to check
|
|
22
|
+
* @param now - Current timestamp in milliseconds (defaults to Date.now())
|
|
23
|
+
* @returns true if the entry is valid, false if expired
|
|
24
|
+
*/
|
|
25
|
+
export function isEntryValid(entry: ICacheEntryMeta, now: number = Date.now()): boolean {
|
|
26
|
+
return entry.expiresAt === undefined || entry.expiresAt >= now;
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
/**
|
|
@@ -19,7 +37,7 @@ export function getExpiredEntries(
|
|
|
19
37
|
const expired: ICacheEntryMeta[] = [];
|
|
20
38
|
|
|
21
39
|
for (const entry of Object.values(index.entries)) {
|
|
22
|
-
if (entry
|
|
40
|
+
if (isEntryExpired(entry, now)) {
|
|
23
41
|
expired.push(entry);
|
|
24
42
|
}
|
|
25
43
|
}
|
|
@@ -62,10 +80,14 @@ export function getLruPruneTargets(
|
|
|
62
80
|
/**
|
|
63
81
|
* Creates a new index with the specified entries removed.
|
|
64
82
|
* Returns a new ICacheIndex without mutating the original.
|
|
83
|
+
* @param index - The source index
|
|
84
|
+
* @param keysToRemove - Keys to remove from the index
|
|
85
|
+
* @param now - Optional timestamp for lastModifiedAt (defaults to Date.now())
|
|
65
86
|
*/
|
|
66
87
|
export function removeEntriesFromIndex(
|
|
67
88
|
index: ICacheIndex,
|
|
68
|
-
keysToRemove: ReadonlyArray<string
|
|
89
|
+
keysToRemove: ReadonlyArray<string>,
|
|
90
|
+
now: number = Date.now()
|
|
69
91
|
): ICacheIndex {
|
|
70
92
|
const keySet = new Set(keysToRemove);
|
|
71
93
|
const newEntries: Record<string, ICacheEntryMeta> = {};
|
|
@@ -79,29 +101,36 @@ export function removeEntriesFromIndex(
|
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
return {
|
|
82
|
-
version:
|
|
104
|
+
version: INDEX_VERSION,
|
|
83
105
|
entries: newEntries,
|
|
84
106
|
totalSizeBytes: newTotalSize,
|
|
85
|
-
lastModifiedAt:
|
|
107
|
+
lastModifiedAt: now,
|
|
86
108
|
};
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
/**
|
|
90
112
|
* Adds or updates an entry in the index.
|
|
91
113
|
* Returns a new ICacheIndex without mutating the original.
|
|
114
|
+
* @param index - The source index
|
|
115
|
+
* @param entry - The entry to add or update
|
|
116
|
+
* @param now - Optional timestamp for lastModifiedAt (defaults to Date.now())
|
|
92
117
|
*/
|
|
93
|
-
export function addEntryToIndex(
|
|
118
|
+
export function addEntryToIndex(
|
|
119
|
+
index: ICacheIndex,
|
|
120
|
+
entry: ICacheEntryMeta,
|
|
121
|
+
now: number = Date.now()
|
|
122
|
+
): ICacheIndex {
|
|
94
123
|
const existingEntry = index.entries[entry.key];
|
|
95
124
|
const sizeDelta = entry.sizeBytes - (existingEntry?.sizeBytes ?? 0);
|
|
96
125
|
|
|
97
126
|
return {
|
|
98
|
-
version:
|
|
127
|
+
version: INDEX_VERSION,
|
|
99
128
|
entries: {
|
|
100
129
|
...index.entries,
|
|
101
130
|
[entry.key]: entry,
|
|
102
131
|
},
|
|
103
132
|
totalSizeBytes: index.totalSizeBytes + sizeDelta,
|
|
104
|
-
lastModifiedAt:
|
|
133
|
+
lastModifiedAt: now,
|
|
105
134
|
};
|
|
106
135
|
}
|
|
107
136
|
|
|
@@ -134,12 +163,13 @@ export function touchEntry(
|
|
|
134
163
|
|
|
135
164
|
/**
|
|
136
165
|
* Creates an empty cache index.
|
|
166
|
+
* @param now - Optional timestamp for lastModifiedAt (defaults to Date.now())
|
|
137
167
|
*/
|
|
138
|
-
export function createEmptyIndex(): ICacheIndex {
|
|
168
|
+
export function createEmptyIndex(now: number = Date.now()): ICacheIndex {
|
|
139
169
|
return {
|
|
140
|
-
version:
|
|
170
|
+
version: INDEX_VERSION,
|
|
141
171
|
entries: {},
|
|
142
172
|
totalSizeBytes: 0,
|
|
143
|
-
lastModifiedAt:
|
|
173
|
+
lastModifiedAt: now,
|
|
144
174
|
};
|
|
145
175
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,5 +1,138 @@
|
|
|
1
1
|
import type { IStorageAdapter, TAdapterPath } from "./adapter";
|
|
2
2
|
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4
|
+
// Result Type (for internal use)
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents a successful result.
|
|
9
|
+
*/
|
|
10
|
+
export interface IOk<T> {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
readonly value: T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a failure result.
|
|
17
|
+
*/
|
|
18
|
+
export interface IErr<E> {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly error: E;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Discriminated union for operation results.
|
|
25
|
+
* Enables explicit error handling without exceptions.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* function divide(a: number, b: number): TResult<number, string> {
|
|
30
|
+
* if (b === 0) return { ok: false, error: "Division by zero" };
|
|
31
|
+
* return { ok: true, value: a / b };
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* const result = divide(10, 2);
|
|
35
|
+
* if (result.ok) {
|
|
36
|
+
* console.log(result.value); // 5
|
|
37
|
+
* } else {
|
|
38
|
+
* console.error(result.error);
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export type TResult<T, E = Error> = IOk<T> | IErr<E>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to create a success result.
|
|
46
|
+
*/
|
|
47
|
+
export function ok<T>(value: T): IOk<T> {
|
|
48
|
+
return { ok: true, value };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Helper to create a failure result.
|
|
53
|
+
*/
|
|
54
|
+
export function err<E>(error: E): IErr<E> {
|
|
55
|
+
return { ok: false, error };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────
|
|
59
|
+
// Cache Events (Observability)
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Event emitted when a cache hit occurs.
|
|
64
|
+
*/
|
|
65
|
+
export interface ICacheHitEvent {
|
|
66
|
+
readonly type: "cache_hit";
|
|
67
|
+
readonly key: string;
|
|
68
|
+
readonly sizeBytes: number;
|
|
69
|
+
readonly ageMs: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Event emitted when a cache miss occurs.
|
|
74
|
+
*/
|
|
75
|
+
export interface ICacheMissEvent {
|
|
76
|
+
readonly type: "cache_miss";
|
|
77
|
+
readonly key: string;
|
|
78
|
+
readonly reason: "not_found" | "expired";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Event emitted when an entry is created.
|
|
83
|
+
*/
|
|
84
|
+
export interface ICacheWriteEvent {
|
|
85
|
+
readonly type: "cache_write";
|
|
86
|
+
readonly key: string;
|
|
87
|
+
readonly sizeBytes: number;
|
|
88
|
+
readonly source: "url" | "file" | "blob" | "bytes";
|
|
89
|
+
readonly durationMs: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Event emitted when an entry is removed.
|
|
94
|
+
*/
|
|
95
|
+
export interface ICacheRemoveEvent {
|
|
96
|
+
readonly type: "cache_remove";
|
|
97
|
+
readonly key: string;
|
|
98
|
+
readonly reason: "explicit" | "expired" | "lru";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Event emitted during pruning operations.
|
|
103
|
+
*/
|
|
104
|
+
export interface ICachePruneEvent {
|
|
105
|
+
readonly type: "cache_prune";
|
|
106
|
+
readonly reason: "expired" | "lru";
|
|
107
|
+
readonly removedCount: number;
|
|
108
|
+
readonly freedBytes: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Event emitted on errors (non-fatal).
|
|
113
|
+
*/
|
|
114
|
+
export interface ICacheErrorEvent {
|
|
115
|
+
readonly type: "cache_error";
|
|
116
|
+
readonly operation: string;
|
|
117
|
+
readonly error: Error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Union of all cache events.
|
|
122
|
+
*/
|
|
123
|
+
export type TCacheEvent =
|
|
124
|
+
| ICacheHitEvent
|
|
125
|
+
| ICacheMissEvent
|
|
126
|
+
| ICacheWriteEvent
|
|
127
|
+
| ICacheRemoveEvent
|
|
128
|
+
| ICachePruneEvent
|
|
129
|
+
| ICacheErrorEvent;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Callback for receiving cache events.
|
|
133
|
+
*/
|
|
134
|
+
export type TCacheEventHandler = (event: TCacheEvent) => void;
|
|
135
|
+
|
|
3
136
|
// ─────────────────────────────────────────────────────────────────
|
|
4
137
|
// Cache Configuration
|
|
5
138
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -39,6 +172,29 @@ export interface ICacheConfig {
|
|
|
39
172
|
* @default SHA-256 hex
|
|
40
173
|
*/
|
|
41
174
|
readonly hashFn?: (input: string) => string | Promise<string>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Event handler for observability/instrumentation.
|
|
178
|
+
* Called on cache hits, misses, writes, removes, and errors.
|
|
179
|
+
* @default undefined (no events emitted)
|
|
180
|
+
*/
|
|
181
|
+
readonly onEvent?: TCacheEventHandler;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Custom time source function for testing.
|
|
185
|
+
* Returns current time in milliseconds since epoch.
|
|
186
|
+
* @default Date.now
|
|
187
|
+
*/
|
|
188
|
+
readonly now?: () => number;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Debounce time for index writes in milliseconds.
|
|
192
|
+
* When > 0, index writes are batched and written after this delay.
|
|
193
|
+
* Use 0 for immediate writes (default behavior).
|
|
194
|
+
* Call flush() to force immediate persistence when using debounce.
|
|
195
|
+
* @default 0 (immediate writes)
|
|
196
|
+
*/
|
|
197
|
+
readonly indexWriteDebounceMs?: number;
|
|
42
198
|
}
|
|
43
199
|
|
|
44
200
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -163,3 +319,41 @@ export interface ICacheStats {
|
|
|
163
319
|
readonly oldestEntry?: ICacheEntryMeta;
|
|
164
320
|
readonly newestEntry?: ICacheEntryMeta;
|
|
165
321
|
}
|
|
322
|
+
|
|
323
|
+
// ─────────────────────────────────────────────────────────────────
|
|
324
|
+
// Read-Through Cache (getOrPut)
|
|
325
|
+
// ─────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Fetcher result for getOrPut operations.
|
|
329
|
+
* Provides the data to cache along with optional configuration.
|
|
330
|
+
*/
|
|
331
|
+
export interface IFetcherResult {
|
|
332
|
+
/** The data to cache, as Uint8Array bytes. */
|
|
333
|
+
readonly bytes: Uint8Array;
|
|
334
|
+
/** Optional TTL override for this entry. */
|
|
335
|
+
readonly ttlMs?: number;
|
|
336
|
+
/** Optional file extension (e.g., ".json"). */
|
|
337
|
+
readonly ext?: string;
|
|
338
|
+
/** Optional user metadata. */
|
|
339
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Fetcher function type for getOrPut.
|
|
344
|
+
* Called when the key doesn't exist in cache.
|
|
345
|
+
* Should return the data to be cached, or throw to skip caching.
|
|
346
|
+
*/
|
|
347
|
+
export type TFetcher = (key: string) => Promise<IFetcherResult>;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Result of a getOrPut operation.
|
|
351
|
+
*/
|
|
352
|
+
export interface IGetOrPutResult {
|
|
353
|
+
/** The cached entry. */
|
|
354
|
+
readonly entry: ICacheEntry;
|
|
355
|
+
/** The public URI for accessing the entry. */
|
|
356
|
+
readonly uri: string;
|
|
357
|
+
/** Whether the entry was fetched (true) or already cached (false). */
|
|
358
|
+
readonly fetched: boolean;
|
|
359
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,12 +12,15 @@ export type {
|
|
|
12
12
|
IStorageAdapter,
|
|
13
13
|
TAdapterPath,
|
|
14
14
|
TBinarySource,
|
|
15
|
+
TBinarySourceType,
|
|
15
16
|
IBinaryWriteResult,
|
|
16
17
|
IBinaryWriteOptions,
|
|
17
18
|
IFileStat,
|
|
18
19
|
TProgressCallback,
|
|
19
20
|
} from "./core/adapter";
|
|
20
21
|
|
|
22
|
+
export { adapterSupportsSource } from "./core/adapter";
|
|
23
|
+
|
|
21
24
|
export type {
|
|
22
25
|
ICacheConfig,
|
|
23
26
|
ICacheEntry,
|
|
@@ -26,14 +29,32 @@ export type {
|
|
|
26
29
|
IPutOptions,
|
|
27
30
|
IPutResult,
|
|
28
31
|
IGetResult,
|
|
32
|
+
IGetOrPutResult,
|
|
33
|
+
TFetcher,
|
|
34
|
+
IFetcherResult,
|
|
29
35
|
IListOptions,
|
|
30
36
|
IPruneResult,
|
|
31
37
|
ICacheStats,
|
|
32
38
|
TPutStatus,
|
|
33
39
|
TSortField,
|
|
34
40
|
TSortOrder,
|
|
41
|
+
// Result type utilities
|
|
42
|
+
TResult,
|
|
43
|
+
IOk,
|
|
44
|
+
IErr,
|
|
45
|
+
// Event types for observability
|
|
46
|
+
TCacheEvent,
|
|
47
|
+
TCacheEventHandler,
|
|
48
|
+
ICacheHitEvent,
|
|
49
|
+
ICacheMissEvent,
|
|
50
|
+
ICacheWriteEvent,
|
|
51
|
+
ICacheRemoveEvent,
|
|
52
|
+
ICachePruneEvent,
|
|
53
|
+
ICacheErrorEvent,
|
|
35
54
|
} from "./core/types";
|
|
36
55
|
|
|
56
|
+
export { ok, err } from "./core/types";
|
|
57
|
+
|
|
37
58
|
// ─────────────────────────────────────────────────────────────────
|
|
38
59
|
// Errors
|
|
39
60
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -80,3 +101,4 @@ export type { IMemoryAdapter } from "./adapters/memoryAdapter";
|
|
|
80
101
|
|
|
81
102
|
export { hash, hashSync } from "./core/hash";
|
|
82
103
|
export { Mutex, KeyedMutex } from "./core/mutex";
|
|
104
|
+
export { isEntryExpired, isEntryValid } from "./core/prune";
|