-
Notifications
You must be signed in to change notification settings - Fork 198
/
storage.ts
175 lines (159 loc) 路 5.34 KB
/
storage.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import path from "node:path";
import type Database from "better-sqlite3";
import { Awaitable } from "./sync";
export interface StoredMeta<Meta = unknown> {
/** Unix timestamp in seconds when this key expires */
expiration?: number;
/** Arbitrary JSON-serializable object */
metadata?: Meta;
}
export interface RangeStoredMeta<Meta = unknown> extends StoredMeta<Meta> {
range: {
offset: number;
length: number;
};
}
export interface StoredValue {
value: Uint8Array;
}
export interface RangeStoredValue extends StoredValue {
range: {
offset: number;
length: number;
};
}
export interface StoredKey {
name: string;
}
export type StoredValueMeta<Meta = unknown> = StoredValue & StoredMeta<Meta>;
export type RangeStoredValueMeta<Meta = unknown> = RangeStoredValue &
RangeStoredMeta<Meta>;
export type StoredKeyMeta<Meta = unknown> = StoredKey & StoredMeta<Meta>;
export interface Range {
offset?: number;
length?: number;
suffix?: number;
}
export interface StorageListOptions {
// Stage 1: filtering
/** Returned keys must start with this string if defined */
prefix?: string;
/** Returned keys must be lexicographically >= this string if defined */
start?: string;
/** Returned keys must be lexicographically < this string if defined */
end?: string;
// Stage 2: sorting
/** Return keys in reverse order, MUST be applied before the limit/cursor */
reverse?: boolean;
// Stage 3: paginating
// Motivation for cursor: we want to make sure if keys are added whilst we're
// paginating, they're returned. We could do this by setting `start` to the
// cursor, adding 1 to the limit, and removing the first key if it matches.
// However, this only works if we can increase the limit, which isn't the case
// for remote KV storage. Even with this, we'd still need to return an extra
// pointer from the list result so we knew if there were still more keys. This
// also lets other databases use their own cursors if supported.
/** Cursor for pagination, undefined/empty-string means start at beginning */
cursor?: string;
/** Maximum number of keys to return if defined */
limit?: number;
// Stage 4: filtering
/** If Delimiter, filter all keys containing delimiter and update cursor */
delimiter?: string;
}
export interface StorageListResult<Key extends StoredKey = StoredKeyMeta> {
keys: Key[];
/** Cursor for next page */
cursor: string;
/** DelimitedPrefixes if delimiter */
delimitedPrefixes?: string[];
}
/**
* Common class for key-value storage:
* - Methods should always return fresh copies of data (safe to mutate returned)
* - Methods shouldn't return expired keys
*/
export abstract class Storage {
abstract has(key: string): Awaitable<boolean>;
abstract head<Meta = unknown>(
key: string
): Awaitable<StoredMeta<Meta> | undefined>;
abstract get<Meta = unknown>(
key: string,
skipMetadata?: false
): Awaitable<StoredValueMeta<Meta> | undefined>;
abstract get(
key: string,
skipMetadata: true
): Awaitable<StoredValue | undefined>;
abstract getRange<Meta = unknown>(
key: string,
range?: Range,
skipMetadata?: false
): Awaitable<RangeStoredValueMeta<Meta> | undefined>;
abstract getRange(
key: string,
range: undefined | Range,
skipMetadata: true
): Awaitable<RangeStoredValue | undefined>;
abstract put<Meta = unknown>(
key: string,
value: StoredValueMeta<Meta>
): Awaitable<void>;
abstract delete(key: string): Awaitable<boolean>;
abstract list<Meta = unknown>(
options?: StorageListOptions,
skipMetadata?: false
): Awaitable<StorageListResult<StoredKeyMeta<Meta>>>;
abstract list(
options: StorageListOptions,
skipMetadata: true
): Awaitable<StorageListResult<StoredKey>>;
async getSqliteDatabase(): Promise<Database.Database> {
throw new Error("D1 not implemented for this Storage class");
}
// Batch functions, default implementations may be overridden to optimise
async hasMany(keys: string[]): Promise<number> {
const results = keys.map(this.has.bind(this));
let count = 0;
for (const result of await Promise.all(results)) if (result) count++;
return count;
}
getMany<Meta = unknown>(
keys: string[],
skipMetadata?: false
): Promise<(StoredValueMeta<Meta> | undefined)[]>;
getMany(
keys: string[],
skipMetadata: true
): Promise<(StoredValue | undefined)[]>;
getMany<Meta = unknown>(
keys: string[],
skipMetadata?: boolean
): Promise<(StoredValueMeta<Meta> | undefined)[]> {
return Promise.all(
keys.map((key) => this.get(key, skipMetadata as any))
) as Promise<(StoredValueMeta<Meta> | undefined)[]>;
}
async putMany<Meta = unknown>(
data: [key: string, value: StoredValueMeta<Meta>][]
): Promise<void> {
await Promise.all(data.map(([key, value]) => this.put(key, value)));
}
async deleteMany(keys: string[]): Promise<number> {
const results = keys.map(this.delete.bind(this));
let count = 0;
for (const result of await Promise.all(results)) if (result) count++;
return count;
}
}
export interface StorageFactory {
storage(namespace: string, persist?: boolean | string): Awaitable<Storage>;
dispose?(): Awaitable<void>;
}
export function getSQLiteNativeBindingLocation(sqliteResolvePath: string) {
return path.resolve(
path.dirname(sqliteResolvePath),
"../build/Release/better_sqlite3.node"
);
}