-
Notifications
You must be signed in to change notification settings - Fork 198
/
local.ts
134 lines (122 loc) 路 3.98 KB
/
local.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import {
Awaitable,
BetterSqlite3Exports,
Range,
RangeStoredValueMeta,
Storage,
StorageListOptions,
StorageListResult,
StoredKeyMeta,
StoredMeta,
StoredValueMeta,
defaultClock,
getSQLiteNativeBindingLocation,
millisToSeconds,
} from "@miniflare/shared";
import type Database from "better-sqlite3";
import { npxImport, npxResolve } from "npx-import";
import { listFilterMatch, listPaginate } from "./helpers";
export abstract class LocalStorage extends Storage {
protected constructor(private readonly clock = defaultClock) {
super();
}
abstract hasMaybeExpired(key: string): Awaitable<StoredMeta | undefined>;
abstract headMaybeExpired<Meta>(
key: string
): Awaitable<StoredMeta<Meta> | undefined>;
abstract getMaybeExpired<Meta>(
key: string
): Awaitable<StoredValueMeta<Meta> | undefined>;
abstract getRangeMaybeExpired<Meta>(
key: string,
range: Range
): Awaitable<RangeStoredValueMeta<Meta> | undefined>;
abstract deleteMaybeExpired(key: string): Awaitable<boolean>;
abstract listAllMaybeExpired<Meta>(): Awaitable<StoredKeyMeta<Meta>[]>;
private expired({ expiration }: StoredMeta, time = this.clock()): boolean {
return expiration !== undefined && expiration <= millisToSeconds(time);
}
async has(key: string): Promise<boolean> {
const stored = await this.hasMaybeExpired(key);
if (stored === undefined) return false;
if (this.expired(stored)) {
await this.deleteMaybeExpired(key);
return false;
}
return true;
}
async head<Meta = unknown>(
key: string
): Promise<StoredMeta<Meta> | undefined> {
const stored = await this.headMaybeExpired<Meta>(key);
if (stored === undefined) return undefined;
if (this.expired(stored)) {
await this.deleteMaybeExpired(key);
return undefined;
}
return stored;
}
async get<Meta = unknown>(
key: string
): Promise<StoredValueMeta<Meta> | undefined> {
const stored = await this.getMaybeExpired<Meta>(key);
if (stored === undefined) return undefined;
if (this.expired(stored)) {
await this.deleteMaybeExpired(key);
return undefined;
}
return stored;
}
async getRange<Meta = unknown>(
key: string,
range: Range = {}
): Promise<RangeStoredValueMeta<Meta> | undefined> {
const stored = await this.getRangeMaybeExpired<Meta>(key, range);
if (stored === undefined) return undefined;
if (this.expired(stored)) {
await this.deleteMaybeExpired(key);
return undefined;
}
return stored;
}
async delete(key: string): Promise<boolean> {
const stored = await this.hasMaybeExpired(key);
const expired = stored !== undefined && this.expired(stored);
const deleted = await this.deleteMaybeExpired(key);
// TOCTTOU: not using `stored` to determine if file existed in first place,
// just whether it had expired before deleting
if (!deleted) return false;
return !expired;
}
async list<Meta = unknown>(
options?: StorageListOptions
): Promise<StorageListResult<StoredKeyMeta<Meta>>> {
const time = this.clock();
const deletePromises: Awaitable<boolean>[] = [];
// Fetch all keys
let keys = await this.listAllMaybeExpired<Meta>();
// Filter out expired and non-matching keys
keys = keys.filter((stored) => {
if (this.expired(stored, time)) {
deletePromises.push(this.deleteMaybeExpired(stored.name));
return false;
}
// Apply prefix, start, and end filter
return listFilterMatch(options, stored.name);
});
// Apply sort, cursor, limit, and delimiter
const res = listPaginate(options, keys);
await Promise.all(deletePromises);
return res;
}
async getSqliteDatabase(): Promise<Database.Database> {
const DatabaseConstructor = await npxImport<
typeof import("better-sqlite3")
>("better-sqlite3@^7.5.3");
return new DatabaseConstructor(":memory:", {
nativeBinding: getSQLiteNativeBindingLocation(
npxResolve("better-sqlite3")
),
});
}
}