polystore 0.15.13 → 0.16.1
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/index.d.ts +243 -0
- package/index.js +859 -0
- package/package.json +20 -24
- package/readme.md +271 -39
- package/src/clients/Client.js +0 -7
- package/src/clients/api.js +0 -34
- package/src/clients/cloudflare.js +0 -56
- package/src/clients/cookie.js +0 -51
- package/src/clients/etcd.js +0 -29
- package/src/clients/file.js +0 -70
- package/src/clients/folder.js +0 -49
- package/src/clients/forage.js +0 -29
- package/src/clients/index.js +0 -25
- package/src/clients/level.js +0 -40
- package/src/clients/memory.js +0 -19
- package/src/clients/redis.js +0 -51
- package/src/clients/storage.js +0 -25
- package/src/index.d.ts +0 -209
- package/src/index.js +0 -278
- package/src/server.js +0 -81
- package/src/utils.js +0 -47
package/index.js
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
// src/clients/Client.ts
|
|
2
|
+
var Client = class {
|
|
3
|
+
EXPIRES = false;
|
|
4
|
+
client;
|
|
5
|
+
encode = (val) => JSON.stringify(val, null, 2);
|
|
6
|
+
decode = (val) => val ? JSON.parse(val) : null;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/clients/api.ts
|
|
13
|
+
var Api = class extends Client {
|
|
14
|
+
// Indicate that the file handler DOES handle expirations
|
|
15
|
+
EXPIRES = true;
|
|
16
|
+
static test = (client) => typeof client === "string" && /^https?:\/\//.test(client);
|
|
17
|
+
#api = async (key, opts = "", method = "GET", body) => {
|
|
18
|
+
const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
|
|
19
|
+
const headers = {
|
|
20
|
+
accept: "application/json",
|
|
21
|
+
"content-type": "application/json"
|
|
22
|
+
};
|
|
23
|
+
const res = await fetch(url, { method, headers, body });
|
|
24
|
+
if (!res.ok) return null;
|
|
25
|
+
return this.decode(await res.text());
|
|
26
|
+
};
|
|
27
|
+
get = (key) => this.#api(key);
|
|
28
|
+
set = async (key, value, { expires } = {}) => {
|
|
29
|
+
const exp = typeof expires === "number" ? `?expires=${expires}` : "";
|
|
30
|
+
await this.#api(key, exp, "PUT", this.encode(value));
|
|
31
|
+
};
|
|
32
|
+
del = async (key) => {
|
|
33
|
+
await this.#api(key, "", "DELETE");
|
|
34
|
+
};
|
|
35
|
+
async *iterate(prefix = "") {
|
|
36
|
+
const data = await this.#api("", `?prefix=${encodeURIComponent(prefix)}`);
|
|
37
|
+
for (let [key, value] of Object.entries(data || {})) {
|
|
38
|
+
if (value !== null && value !== void 0) {
|
|
39
|
+
yield [prefix + key, value];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/clients/cloudflare.ts
|
|
46
|
+
var Cloudflare = class extends Client {
|
|
47
|
+
// It handles expirations natively
|
|
48
|
+
EXPIRES = true;
|
|
49
|
+
// Check whether the given store is a FILE-type
|
|
50
|
+
static test = (client) => client?.constructor?.name === "KvNamespace" || client?.constructor?.name === "EdgeKVNamespace";
|
|
51
|
+
get = async (key) => this.decode(await this.client.get(key));
|
|
52
|
+
set = (key, data, opts) => {
|
|
53
|
+
const expirationTtl = opts.expires ? Math.round(opts.expires) : void 0;
|
|
54
|
+
if (expirationTtl && expirationTtl < 60) {
|
|
55
|
+
throw new Error("Cloudflare's min expiration is '60s'");
|
|
56
|
+
}
|
|
57
|
+
return this.client.put(key, this.encode(data), { expirationTtl });
|
|
58
|
+
};
|
|
59
|
+
del = (key) => this.client.delete(key);
|
|
60
|
+
// Since we have pagination, we don't want to get all of the
|
|
61
|
+
// keys at once if we can avoid it
|
|
62
|
+
async *iterate(prefix = "") {
|
|
63
|
+
let cursor;
|
|
64
|
+
do {
|
|
65
|
+
const raw = await this.client.list({ prefix, cursor });
|
|
66
|
+
const keys = raw.keys.map((k) => k.name);
|
|
67
|
+
for (let key of keys) {
|
|
68
|
+
const value = await this.get(key);
|
|
69
|
+
if (value !== null && value !== void 0) yield [key, value];
|
|
70
|
+
}
|
|
71
|
+
cursor = raw.list_complete ? void 0 : raw.cursor;
|
|
72
|
+
} while (cursor);
|
|
73
|
+
}
|
|
74
|
+
keys = async (prefix = "") => {
|
|
75
|
+
const keys = [];
|
|
76
|
+
let cursor;
|
|
77
|
+
do {
|
|
78
|
+
const raw = await this.client.list({ prefix, cursor });
|
|
79
|
+
keys.push(...raw.keys.map((k) => k.name));
|
|
80
|
+
cursor = raw.list_complete ? void 0 : raw.cursor;
|
|
81
|
+
} while (cursor);
|
|
82
|
+
return keys;
|
|
83
|
+
};
|
|
84
|
+
entries = async (prefix = "") => {
|
|
85
|
+
const keys = await this.keys(prefix);
|
|
86
|
+
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
87
|
+
return keys.map((k, i) => [k, values[i]]);
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/clients/cookie.ts
|
|
92
|
+
var Cookie = class extends Client {
|
|
93
|
+
// It handles expirations natively
|
|
94
|
+
EXPIRES = true;
|
|
95
|
+
// Check if this is the right class for the given client
|
|
96
|
+
static test = (client) => {
|
|
97
|
+
return client === "cookie" || client === "cookies";
|
|
98
|
+
};
|
|
99
|
+
// Group methods
|
|
100
|
+
#read = () => {
|
|
101
|
+
const all = {};
|
|
102
|
+
for (let entry of document.cookie.split(";")) {
|
|
103
|
+
try {
|
|
104
|
+
const [rawKey, rawValue] = entry.split("=");
|
|
105
|
+
const key = decodeURIComponent(rawKey.trim());
|
|
106
|
+
const value = this.decode(decodeURIComponent(rawValue.trim()));
|
|
107
|
+
all[key] = value;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return all;
|
|
112
|
+
};
|
|
113
|
+
// For cookies, an empty value is the same as null, even `""`
|
|
114
|
+
get = (key) => {
|
|
115
|
+
const all = this.#read();
|
|
116
|
+
return key in all ? all[key] : null;
|
|
117
|
+
};
|
|
118
|
+
set = (key, data, { expires }) => {
|
|
119
|
+
const k = encodeURIComponent(key);
|
|
120
|
+
const value = encodeURIComponent(this.encode(data ?? ""));
|
|
121
|
+
let exp = "";
|
|
122
|
+
if (typeof expires === "number") {
|
|
123
|
+
const when = expires <= 0 ? 0 : Date.now() + expires * 1e3;
|
|
124
|
+
exp = `; expires=${new Date(when).toUTCString()}`;
|
|
125
|
+
}
|
|
126
|
+
document.cookie = `${k}=${value}${exp}`;
|
|
127
|
+
};
|
|
128
|
+
del = (key) => this.set(key, "", { expires: -100 });
|
|
129
|
+
async *iterate(prefix = "") {
|
|
130
|
+
for (let [key, value] of Object.entries(this.#read())) {
|
|
131
|
+
if (!key.startsWith(prefix)) continue;
|
|
132
|
+
yield [key, value];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/clients/etcd.ts
|
|
138
|
+
var Etcd = class extends Client {
|
|
139
|
+
// It desn't handle expirations natively
|
|
140
|
+
EXPIRES = false;
|
|
141
|
+
// Check if this is the right class for the given client
|
|
142
|
+
static test = (client) => client?.constructor?.name === "Etcd3";
|
|
143
|
+
get = (key) => this.client.get(key).json();
|
|
144
|
+
set = (key, value) => this.client.put(key).value(this.encode(value));
|
|
145
|
+
del = (key) => this.client.delete().key(key).exec();
|
|
146
|
+
async *iterate(prefix = "") {
|
|
147
|
+
const keys = await this.client.getAll().prefix(prefix).keys();
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
yield [key, await this.get(key)];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
keys = (prefix = "") => this.client.getAll().prefix(prefix).keys();
|
|
153
|
+
entries = async (prefix = "") => {
|
|
154
|
+
const keys = await this.keys(prefix);
|
|
155
|
+
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
156
|
+
return keys.map((k, i) => [k, values[i]]);
|
|
157
|
+
};
|
|
158
|
+
clear = async (prefix = "") => {
|
|
159
|
+
if (!prefix) return this.client.delete().all();
|
|
160
|
+
return this.client.delete().prefix(prefix);
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/clients/file.ts
|
|
165
|
+
var File = class extends Client {
|
|
166
|
+
// It desn't handle expirations natively
|
|
167
|
+
EXPIRES = false;
|
|
168
|
+
fsp;
|
|
169
|
+
file = "";
|
|
170
|
+
#lock = Promise.resolve();
|
|
171
|
+
// Check if this is the right class for the given client
|
|
172
|
+
static test = (client) => {
|
|
173
|
+
if (client instanceof URL) client = client.href;
|
|
174
|
+
return typeof client === "string" && client.startsWith("file://") && client.includes(".");
|
|
175
|
+
};
|
|
176
|
+
// We want to make sure the file already exists, so attempt to
|
|
177
|
+
// create the folders and the file (but not OVERWRITE it, that's why the x flag)
|
|
178
|
+
// It fails if it already exists, hence the catch case
|
|
179
|
+
promise = (async () => {
|
|
180
|
+
this.fsp = await import("fs/promises");
|
|
181
|
+
this.file = (this.client?.href || this.client).replace(/^file:\/\//, "");
|
|
182
|
+
const folder = this.file.split("/").slice(0, -1).join("/");
|
|
183
|
+
await this.fsp.mkdir(folder, { recursive: true }).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
await this.fsp.writeFile(this.file, "{}", { flag: "wx" }).catch(() => {
|
|
186
|
+
});
|
|
187
|
+
})();
|
|
188
|
+
// Internal - acquire lock before operations
|
|
189
|
+
#withLock = async (fn) => {
|
|
190
|
+
const previousLock = this.#lock;
|
|
191
|
+
let releaseLock;
|
|
192
|
+
this.#lock = new Promise((resolve) => {
|
|
193
|
+
releaseLock = resolve;
|
|
194
|
+
});
|
|
195
|
+
try {
|
|
196
|
+
await previousLock;
|
|
197
|
+
return await fn();
|
|
198
|
+
} finally {
|
|
199
|
+
releaseLock();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
#read = async () => {
|
|
203
|
+
try {
|
|
204
|
+
const text = await this.fsp.readFile(this.file, "utf8");
|
|
205
|
+
return text ? JSON.parse(text) : {};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error.code === "ENOENT") return {};
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
#write = async (data) => {
|
|
212
|
+
return this.fsp.writeFile(this.file, this.encode(data));
|
|
213
|
+
};
|
|
214
|
+
get = async (key) => {
|
|
215
|
+
return this.#withLock(async () => {
|
|
216
|
+
const data = await this.#read();
|
|
217
|
+
return data[key] ?? null;
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
set = async (key, value) => {
|
|
221
|
+
return this.#withLock(async () => {
|
|
222
|
+
const data = await this.#read();
|
|
223
|
+
if (value === null) {
|
|
224
|
+
delete data[key];
|
|
225
|
+
} else {
|
|
226
|
+
data[key] = value;
|
|
227
|
+
}
|
|
228
|
+
await this.#write(data);
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
async *iterate(prefix = "") {
|
|
232
|
+
const data = await this.#read();
|
|
233
|
+
const entries = Object.entries(data).filter((p) => p[0].startsWith(prefix));
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
yield entry;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Bulk updates are worth creating a custom method here
|
|
239
|
+
clearAll = () => this.#withLock(() => this.#write({}));
|
|
240
|
+
clear = async (prefix = "") => {
|
|
241
|
+
return this.#withLock(async () => {
|
|
242
|
+
const data = await this.#read();
|
|
243
|
+
for (let key in data) {
|
|
244
|
+
if (key.startsWith(prefix)) {
|
|
245
|
+
delete data[key];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
await this.#write(data);
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/clients/folder.ts
|
|
254
|
+
var noFileOk = (error) => {
|
|
255
|
+
if (error.code === "ENOENT") return null;
|
|
256
|
+
throw error;
|
|
257
|
+
};
|
|
258
|
+
var Folder = class extends Client {
|
|
259
|
+
// It desn't handle expirations natively
|
|
260
|
+
EXPIRES = false;
|
|
261
|
+
fsp;
|
|
262
|
+
folder;
|
|
263
|
+
// Check if this is the right class for the given client
|
|
264
|
+
static test = (client) => {
|
|
265
|
+
if (client instanceof URL) client = client.href;
|
|
266
|
+
return typeof client === "string" && client.startsWith("file://") && client.endsWith("/");
|
|
267
|
+
};
|
|
268
|
+
// Make sure the folder already exists, so attempt to create it
|
|
269
|
+
// It fails if it already exists, hence the catch case
|
|
270
|
+
promise = (async () => {
|
|
271
|
+
this.fsp = await import("fs/promises");
|
|
272
|
+
this.folder = (this.client?.href || this.client).replace(/^file:\/\//, "");
|
|
273
|
+
await this.fsp.mkdir(this.folder, { recursive: true }).catch(() => {
|
|
274
|
+
});
|
|
275
|
+
})();
|
|
276
|
+
file = (key) => this.folder + key + ".json";
|
|
277
|
+
get = (key) => {
|
|
278
|
+
return this.fsp.readFile(this.file(key), "utf8").then(this.decode, noFileOk);
|
|
279
|
+
};
|
|
280
|
+
set = (key, value) => {
|
|
281
|
+
return this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
|
|
282
|
+
};
|
|
283
|
+
del = (key) => this.fsp.unlink(this.file(key)).catch(noFileOk);
|
|
284
|
+
async *iterate(prefix = "") {
|
|
285
|
+
const all = await this.fsp.readdir(this.folder);
|
|
286
|
+
const keys = all.filter((f) => f.startsWith(prefix) && f.endsWith(".json"));
|
|
287
|
+
for (const name of keys) {
|
|
288
|
+
const key = name.slice(0, -".json".length);
|
|
289
|
+
try {
|
|
290
|
+
const data = await this.get(key);
|
|
291
|
+
yield [key, data];
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/clients/forage.ts
|
|
300
|
+
var Forage = class extends Client {
|
|
301
|
+
// It desn't handle expirations natively
|
|
302
|
+
EXPIRES = false;
|
|
303
|
+
// Check if this is the right class for the given client
|
|
304
|
+
static test = (client) => client?.defineDriver && client?.dropInstance && client?.INDEXEDDB;
|
|
305
|
+
get = (key) => this.client.getItem(key);
|
|
306
|
+
set = (key, value) => this.client.setItem(key, value);
|
|
307
|
+
del = (key) => this.client.removeItem(key);
|
|
308
|
+
async *iterate(prefix = "") {
|
|
309
|
+
const keys = await this.client.keys();
|
|
310
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
311
|
+
for (const key of list) {
|
|
312
|
+
const value = await this.get(key);
|
|
313
|
+
if (value !== null && value !== void 0) {
|
|
314
|
+
yield [key, value];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
entries = async (prefix = "") => {
|
|
319
|
+
const all = await this.client.keys();
|
|
320
|
+
const keys = all.filter((k) => k.startsWith(prefix));
|
|
321
|
+
const values = await Promise.all(keys.map((key) => this.get(key)));
|
|
322
|
+
return keys.map((key, i) => [key, values[i]]);
|
|
323
|
+
};
|
|
324
|
+
clearAll = () => this.client.clear();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/clients/level.ts
|
|
328
|
+
var valueEncoding = "json";
|
|
329
|
+
var notFound = (error) => {
|
|
330
|
+
if (error?.code === "LEVEL_NOT_FOUND") return null;
|
|
331
|
+
throw error;
|
|
332
|
+
};
|
|
333
|
+
var Level = class extends Client {
|
|
334
|
+
// It desn't handle expirations natively
|
|
335
|
+
EXPIRES = false;
|
|
336
|
+
// Check if this is the right class for the given client
|
|
337
|
+
static test = (client) => client?.constructor?.name === "ClassicLevel";
|
|
338
|
+
get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
|
|
339
|
+
set = (key, value) => this.client.put(key, value, { valueEncoding });
|
|
340
|
+
del = (key) => this.client.del(key);
|
|
341
|
+
async *iterate(prefix = "") {
|
|
342
|
+
const keys = await this.client.keys().all();
|
|
343
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
344
|
+
for (const key of list) {
|
|
345
|
+
yield [key, await this.get(key)];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
entries = async (prefix = "") => {
|
|
349
|
+
const keys = await this.client.keys().all();
|
|
350
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
351
|
+
return Promise.all(
|
|
352
|
+
list.map(async (k) => [k, await this.get(k)])
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
clearAll = () => this.client.clear();
|
|
356
|
+
clear = async (prefix = "") => {
|
|
357
|
+
const keys = await this.client.keys().all();
|
|
358
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
359
|
+
return this.client.batch(
|
|
360
|
+
list.map((key) => ({ type: "del", key }))
|
|
361
|
+
);
|
|
362
|
+
};
|
|
363
|
+
close = () => this.client.close();
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/clients/memory.ts
|
|
367
|
+
var Memory = class extends Client {
|
|
368
|
+
// It desn't handle expirations natively
|
|
369
|
+
EXPIRES = false;
|
|
370
|
+
// Check if this is the right class for the given client
|
|
371
|
+
static test = (client) => client instanceof Map;
|
|
372
|
+
get = (key) => this.client.get(key) ?? null;
|
|
373
|
+
set = (key, data) => this.client.set(key, data);
|
|
374
|
+
del = (key) => this.client.delete(key);
|
|
375
|
+
*iterate(prefix = "") {
|
|
376
|
+
for (const entry of this.client.entries()) {
|
|
377
|
+
if (entry[0].startsWith(prefix)) yield entry;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
clearAll = () => this.client.clear();
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/clients/redis.ts
|
|
384
|
+
var Redis = class extends Client {
|
|
385
|
+
// Indicate if this client handles expirations (true = it does)
|
|
386
|
+
EXPIRES = true;
|
|
387
|
+
// Check if this is the right class for the given client
|
|
388
|
+
static test = (client) => client && client.pSubscribe && client.sSubscribe;
|
|
389
|
+
get = async (key) => this.decode(await this.client.get(key));
|
|
390
|
+
set = async (key, value, { expires } = {}) => {
|
|
391
|
+
const EX = expires ? Math.round(expires) : void 0;
|
|
392
|
+
return this.client.set(key, this.encode(value), { EX });
|
|
393
|
+
};
|
|
394
|
+
del = (key) => this.client.del(key);
|
|
395
|
+
has = async (key) => Boolean(await this.client.exists(key));
|
|
396
|
+
// Go through each of the [key, value] in the set
|
|
397
|
+
async *iterate(prefix = "") {
|
|
398
|
+
const MATCH = prefix + "*";
|
|
399
|
+
for await (const key of this.client.scanIterator({ MATCH })) {
|
|
400
|
+
const value = await this.get(key);
|
|
401
|
+
if (value !== null && value !== void 0) {
|
|
402
|
+
yield [key, value];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Optimizing the retrieval of them by not getting their values
|
|
407
|
+
keys = async (prefix = "") => {
|
|
408
|
+
const MATCH = prefix + "*";
|
|
409
|
+
const keys = [];
|
|
410
|
+
for await (const key of this.client.scanIterator({ MATCH })) {
|
|
411
|
+
keys.push(key);
|
|
412
|
+
}
|
|
413
|
+
return keys;
|
|
414
|
+
};
|
|
415
|
+
// Optimizing the retrieval of them all in bulk by loading the values
|
|
416
|
+
// in parallel
|
|
417
|
+
entries = async (prefix = "") => {
|
|
418
|
+
const keys = await this.keys(prefix);
|
|
419
|
+
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
420
|
+
return keys.map((k, i) => [k, values[i]]);
|
|
421
|
+
};
|
|
422
|
+
clearAll = () => this.client.flushAll();
|
|
423
|
+
close = () => this.client.quit();
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/clients/sqlite.ts
|
|
427
|
+
var SQLite = class extends Client {
|
|
428
|
+
// This one is doing manual time management internally even though
|
|
429
|
+
// sqlite does not natively support expirations. This is because it does
|
|
430
|
+
// support creating a `expires_at:Date` column that makes managing
|
|
431
|
+
// expirations much easier, so it's really "somewhere in between"
|
|
432
|
+
EXPIRES = true;
|
|
433
|
+
static test = (client) => typeof client?.prepare === "function";
|
|
434
|
+
get = (id) => {
|
|
435
|
+
const row = this.client.prepare(`SELECT value, expires_at FROM kv WHERE id = ?`).get(id);
|
|
436
|
+
if (!row) return null;
|
|
437
|
+
if (row.expires_at && row.expires_at < Date.now()) {
|
|
438
|
+
this.del(id);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return this.decode(row.value);
|
|
442
|
+
};
|
|
443
|
+
set = (id, data, { expires } = {}) => {
|
|
444
|
+
const value = this.encode(data);
|
|
445
|
+
const expires_at = expires ? Date.now() + expires * 1e3 : null;
|
|
446
|
+
this.client.prepare(
|
|
447
|
+
`INSERT INTO kv (id, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`
|
|
448
|
+
).run(id, value, expires_at);
|
|
449
|
+
};
|
|
450
|
+
del = async (id) => {
|
|
451
|
+
await this.client.prepare(`DELETE FROM kv WHERE id = ?`).run(id);
|
|
452
|
+
};
|
|
453
|
+
has = (id) => {
|
|
454
|
+
const row = this.client.prepare(`SELECT expires_at FROM kv WHERE id = ?`).get(id);
|
|
455
|
+
if (!row) return false;
|
|
456
|
+
if (row.expires_at && row.expires_at < Date.now()) {
|
|
457
|
+
this.del(id);
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
};
|
|
462
|
+
*iterate(prefix = "") {
|
|
463
|
+
this.#clearExpired();
|
|
464
|
+
const sql = `SELECT id, value FROM kv WHERE (expires_at IS NULL OR expires_at > ?) ${prefix ? "AND id LIKE ?" : ""}
|
|
465
|
+
`;
|
|
466
|
+
const params = prefix ? [Date.now(), `${prefix}%`] : [Date.now()];
|
|
467
|
+
for (const row of this.client.prepare(sql).all(...params)) {
|
|
468
|
+
yield [row.id, this.decode(row.value)];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
keys = (prefix = "") => {
|
|
472
|
+
this.#clearExpired();
|
|
473
|
+
const sql = `SELECT id FROM kv WHERE (expires_at IS NULL OR expires_at > ?)
|
|
474
|
+
${prefix ? "AND id LIKE ?" : ""}
|
|
475
|
+
`;
|
|
476
|
+
const params = prefix ? [Date.now(), `${prefix}%`] : [Date.now()];
|
|
477
|
+
const rows = this.client.prepare(sql).all(...params);
|
|
478
|
+
return rows.map((r) => r.id);
|
|
479
|
+
};
|
|
480
|
+
#clearExpired = () => {
|
|
481
|
+
this.client.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
|
|
482
|
+
};
|
|
483
|
+
clearAll = () => {
|
|
484
|
+
this.client.run(`DELETE FROM kv`);
|
|
485
|
+
};
|
|
486
|
+
close = () => {
|
|
487
|
+
this.client.close?.();
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// src/clients/storage.ts
|
|
492
|
+
var WebStorage = class extends Client {
|
|
493
|
+
// It desn't handle expirations natively
|
|
494
|
+
EXPIRES = false;
|
|
495
|
+
// Check if this is the right class for the given client
|
|
496
|
+
static test(client) {
|
|
497
|
+
if (typeof Storage === "undefined") return false;
|
|
498
|
+
return client instanceof Storage;
|
|
499
|
+
}
|
|
500
|
+
// Item methods
|
|
501
|
+
get = (key) => this.decode(this.client.getItem(key));
|
|
502
|
+
set = (key, data) => this.client.setItem(key, this.encode(data));
|
|
503
|
+
del = (key) => this.client.removeItem(key);
|
|
504
|
+
*iterate(prefix = "") {
|
|
505
|
+
for (let i = 0; i < this.client.length; i++) {
|
|
506
|
+
const key = this.client.key(i);
|
|
507
|
+
if (!key || !key.startsWith(prefix)) continue;
|
|
508
|
+
const value = this.get(key);
|
|
509
|
+
if (value !== null && value !== void 0) {
|
|
510
|
+
yield [key, value];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
clearAll = () => this.client.clear();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/clients/index.ts
|
|
518
|
+
var clients_default = {
|
|
519
|
+
api: Api,
|
|
520
|
+
cloudflare: Cloudflare,
|
|
521
|
+
cookie: Cookie,
|
|
522
|
+
etcd: Etcd,
|
|
523
|
+
file: File,
|
|
524
|
+
folder: Folder,
|
|
525
|
+
forage: Forage,
|
|
526
|
+
level: Level,
|
|
527
|
+
memory: Memory,
|
|
528
|
+
// postgres,
|
|
529
|
+
// prisma,
|
|
530
|
+
redis: Redis,
|
|
531
|
+
storage: WebStorage,
|
|
532
|
+
sqlite: SQLite
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// src/utils.ts
|
|
536
|
+
var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
|
|
537
|
+
var parse = function(str) {
|
|
538
|
+
if (str === null || str === void 0) return null;
|
|
539
|
+
if (typeof str === "number") return str;
|
|
540
|
+
const cleaned = str.toLowerCase().replace(/[,_]/g, "");
|
|
541
|
+
let [_, value, units] = times.exec(cleaned) || [];
|
|
542
|
+
if (!units) return null;
|
|
543
|
+
const unitValue = parse[units] || parse[units.replace(/s$/, "")];
|
|
544
|
+
if (!unitValue) return null;
|
|
545
|
+
const result = unitValue * parseFloat(value);
|
|
546
|
+
return Math.abs(Math.round(result * 1e3) / 1e3);
|
|
547
|
+
};
|
|
548
|
+
parse.millisecond = parse.ms = 1e-3;
|
|
549
|
+
parse.second = parse.sec = parse.s = parse[""] = 1;
|
|
550
|
+
parse.minute = parse.min = parse.m = parse.s * 60;
|
|
551
|
+
parse.hour = parse.hr = parse.h = parse.m * 60;
|
|
552
|
+
parse.day = parse.d = parse.h * 24;
|
|
553
|
+
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
554
|
+
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
555
|
+
parse.month = parse.b = parse.y / 12;
|
|
556
|
+
var urlAlphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
557
|
+
function createId() {
|
|
558
|
+
let size = 24;
|
|
559
|
+
let id = "";
|
|
560
|
+
let bytes = crypto.getRandomValues(new Uint8Array(size));
|
|
561
|
+
while (size--) {
|
|
562
|
+
id += urlAlphabet[bytes[size] & 61];
|
|
563
|
+
}
|
|
564
|
+
return id;
|
|
565
|
+
}
|
|
566
|
+
function unix(expires) {
|
|
567
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
568
|
+
return expires === null ? null : now + expires * 1e3;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/index.ts
|
|
572
|
+
var Store = class _Store {
|
|
573
|
+
PREFIX = "";
|
|
574
|
+
promise;
|
|
575
|
+
client;
|
|
576
|
+
constructor(clientPromise = /* @__PURE__ */ new Map()) {
|
|
577
|
+
this.promise = Promise.resolve(clientPromise).then(async (client) => {
|
|
578
|
+
this.client = this.#find(client);
|
|
579
|
+
this.#validate(this.client);
|
|
580
|
+
this.promise = null;
|
|
581
|
+
await this.client.promise;
|
|
582
|
+
return client;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
#find(store) {
|
|
586
|
+
if (store instanceof _Store) return store.client;
|
|
587
|
+
for (let client of Object.values(clients_default)) {
|
|
588
|
+
if (client.test && client.test(store)) {
|
|
589
|
+
return new client(store);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (typeof store === "function" && /^class\s/.test(Function.prototype.toString.call(store))) {
|
|
593
|
+
return new store();
|
|
594
|
+
}
|
|
595
|
+
return store;
|
|
596
|
+
}
|
|
597
|
+
#validate(client) {
|
|
598
|
+
if (!client) throw new Error("No client received");
|
|
599
|
+
if (!client.set || !client.get || !client.iterate) {
|
|
600
|
+
throw new Error("Client should have .get(), .set() and .iterate()");
|
|
601
|
+
}
|
|
602
|
+
if (client.EXPIRES) return;
|
|
603
|
+
for (let method of ["has", "keys", "values"]) {
|
|
604
|
+
if (client[method]) {
|
|
605
|
+
const msg = `You can only define client.${method}() when the client manages the expiration.`;
|
|
606
|
+
throw new Error(msg);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Check if the given data is fresh or not; if
|
|
611
|
+
#isFresh(data, key) {
|
|
612
|
+
if (!data || typeof data !== "object" || !("value" in data)) {
|
|
613
|
+
if (key) this.del(key);
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
if (data.expires === null) return true;
|
|
617
|
+
if (data.expires > Date.now()) return true;
|
|
618
|
+
if (key) this.del(key);
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
async add(value, options = {}) {
|
|
622
|
+
await this.promise;
|
|
623
|
+
let expires = parse(options.expires);
|
|
624
|
+
if (this.client.add) {
|
|
625
|
+
if (this.client.EXPIRES) {
|
|
626
|
+
return await this.client.add(this.PREFIX, value, { expires });
|
|
627
|
+
}
|
|
628
|
+
expires = unix(expires);
|
|
629
|
+
const key2 = await this.client.add(this.PREFIX, { expires, value });
|
|
630
|
+
return key2;
|
|
631
|
+
}
|
|
632
|
+
const key = createId();
|
|
633
|
+
return this.set(key, value, { expires });
|
|
634
|
+
}
|
|
635
|
+
async set(key, value, options = {}) {
|
|
636
|
+
await this.promise;
|
|
637
|
+
const id = this.PREFIX + key;
|
|
638
|
+
let expires = parse(options.expires);
|
|
639
|
+
if (value === null || typeof expires === "number" && expires <= 0) {
|
|
640
|
+
return this.del(key);
|
|
641
|
+
}
|
|
642
|
+
if (this.client.EXPIRES) {
|
|
643
|
+
await this.client.set(id, value, { expires });
|
|
644
|
+
return key;
|
|
645
|
+
}
|
|
646
|
+
expires = unix(expires);
|
|
647
|
+
await this.client.set(id, { expires, value });
|
|
648
|
+
return key;
|
|
649
|
+
}
|
|
650
|
+
async get(key) {
|
|
651
|
+
await this.promise;
|
|
652
|
+
const id = this.PREFIX + key;
|
|
653
|
+
if (this.client.EXPIRES) {
|
|
654
|
+
const data = await this.client.get(id) ?? null;
|
|
655
|
+
if (data === null) return null;
|
|
656
|
+
return data;
|
|
657
|
+
} else {
|
|
658
|
+
const data = await this.client.get(id) ?? null;
|
|
659
|
+
if (data === null) return null;
|
|
660
|
+
if (!this.#isFresh(data, key)) return null;
|
|
661
|
+
return data.value;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check whether a key exists or not:
|
|
666
|
+
*
|
|
667
|
+
* ```js
|
|
668
|
+
* if (await store.has("key1")) { ... }
|
|
669
|
+
* ```
|
|
670
|
+
*
|
|
671
|
+
* If you are going to use the value, it's better to just read it:
|
|
672
|
+
*
|
|
673
|
+
* ```js
|
|
674
|
+
* const val = await store.get("key1");
|
|
675
|
+
* if (val) { ... }
|
|
676
|
+
* ```
|
|
677
|
+
*
|
|
678
|
+
* **[→ Full .has() Docs](https://polystore.dev/documentation#has)**
|
|
679
|
+
*/
|
|
680
|
+
async has(key) {
|
|
681
|
+
await this.promise;
|
|
682
|
+
const id = this.PREFIX + key;
|
|
683
|
+
if (this.client.has) {
|
|
684
|
+
return this.client.has(id);
|
|
685
|
+
}
|
|
686
|
+
return await this.get(key) !== null;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Remove a single key and its value from the store:
|
|
690
|
+
*
|
|
691
|
+
* ```js
|
|
692
|
+
* const key = await store.del("key1");
|
|
693
|
+
* ```
|
|
694
|
+
*
|
|
695
|
+
* **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
|
|
696
|
+
*/
|
|
697
|
+
async del(key) {
|
|
698
|
+
await this.promise;
|
|
699
|
+
const id = this.PREFIX + key;
|
|
700
|
+
if (this.client.del) {
|
|
701
|
+
await this.client.del(id);
|
|
702
|
+
return key;
|
|
703
|
+
}
|
|
704
|
+
if (this.client.EXPIRES) {
|
|
705
|
+
await this.client.set(id, null, { expires: 0 });
|
|
706
|
+
} else {
|
|
707
|
+
await this.client.set(id, null);
|
|
708
|
+
}
|
|
709
|
+
return key;
|
|
710
|
+
}
|
|
711
|
+
async *[Symbol.asyncIterator]() {
|
|
712
|
+
await this.promise;
|
|
713
|
+
if (this.client.EXPIRES) {
|
|
714
|
+
for await (const [name, data] of this.client.iterate(this.PREFIX)) {
|
|
715
|
+
const key = name.slice(this.PREFIX.length);
|
|
716
|
+
yield [key, data];
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
for await (const [name, data] of this.client.iterate(this.PREFIX)) {
|
|
721
|
+
const key = name.slice(this.PREFIX.length);
|
|
722
|
+
if (this.#isFresh(data, key)) {
|
|
723
|
+
yield [key, data.value];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async entries() {
|
|
728
|
+
await this.promise;
|
|
729
|
+
const trim = (key) => key.slice(this.PREFIX.length);
|
|
730
|
+
if (this.client.entries) {
|
|
731
|
+
if (this.client.EXPIRES) {
|
|
732
|
+
const entries = await this.client.entries(this.PREFIX);
|
|
733
|
+
return entries.map(([k, v]) => [trim(k), v]);
|
|
734
|
+
} else {
|
|
735
|
+
const entries = await this.client.entries(this.PREFIX);
|
|
736
|
+
return entries.map(([k, v]) => [trim(k), v]).filter(([key, data]) => this.#isFresh(data, key)).map(([key, data]) => [key, data.value]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (this.client.EXPIRES) {
|
|
740
|
+
const list = [];
|
|
741
|
+
for await (const [k, v] of this.client.iterate(this.PREFIX)) {
|
|
742
|
+
list.push([trim(k), v]);
|
|
743
|
+
}
|
|
744
|
+
return list;
|
|
745
|
+
} else {
|
|
746
|
+
const list = [];
|
|
747
|
+
for await (const [k, data] of this.client.iterate(this.PREFIX)) {
|
|
748
|
+
if (this.#isFresh(data, trim(k))) {
|
|
749
|
+
list.push([trim(k), data.value]);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return list;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Return an array of the keys in the store:
|
|
757
|
+
*
|
|
758
|
+
* ```js
|
|
759
|
+
* const keys = await store.keys();
|
|
760
|
+
* // ["key1", "key2", ...]
|
|
761
|
+
*
|
|
762
|
+
* // To limit it to a given prefix, use `.prefix()`:
|
|
763
|
+
* const sessions = await store.prefix("session:").keys();
|
|
764
|
+
* ```
|
|
765
|
+
*
|
|
766
|
+
* **[→ Full .keys() Docs](https://polystore.dev/documentation#keys)**
|
|
767
|
+
*/
|
|
768
|
+
async keys() {
|
|
769
|
+
await this.promise;
|
|
770
|
+
if (this.client.keys) {
|
|
771
|
+
const list = await this.client.keys(this.PREFIX);
|
|
772
|
+
if (!this.PREFIX) return list;
|
|
773
|
+
return list.map((k) => k.slice(this.PREFIX.length));
|
|
774
|
+
}
|
|
775
|
+
const entries = await this.entries();
|
|
776
|
+
return entries.map((e) => e[0]);
|
|
777
|
+
}
|
|
778
|
+
async values() {
|
|
779
|
+
await this.promise;
|
|
780
|
+
if (this.client.values) {
|
|
781
|
+
if (this.client.EXPIRES) return this.client.values(this.PREFIX);
|
|
782
|
+
const list = await this.client.values(this.PREFIX);
|
|
783
|
+
return list.filter((data) => this.#isFresh(data)).map((data) => data.value);
|
|
784
|
+
}
|
|
785
|
+
const entries = await this.entries();
|
|
786
|
+
return entries.map((e) => e[1]);
|
|
787
|
+
}
|
|
788
|
+
async all() {
|
|
789
|
+
const entries = await this.entries();
|
|
790
|
+
return Object.fromEntries(entries);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Delete all of the records of the store:
|
|
794
|
+
*
|
|
795
|
+
* ```js
|
|
796
|
+
* await store.clear();
|
|
797
|
+
* ```
|
|
798
|
+
*
|
|
799
|
+
* It's useful for cache invalidation, clearing the data, and testing.
|
|
800
|
+
*
|
|
801
|
+
* **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
|
|
802
|
+
*/
|
|
803
|
+
async clear() {
|
|
804
|
+
await this.promise;
|
|
805
|
+
if (!this.PREFIX && this.client.clearAll) {
|
|
806
|
+
return this.client.clearAll();
|
|
807
|
+
}
|
|
808
|
+
if (this.client.clear) {
|
|
809
|
+
return this.client.clear(this.PREFIX);
|
|
810
|
+
}
|
|
811
|
+
const keys = await this.keys();
|
|
812
|
+
await Promise.all(keys.map((key) => this.del(key)));
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Create a substore where all the keys are stored with
|
|
816
|
+
* the given prefix:
|
|
817
|
+
*
|
|
818
|
+
* ```js
|
|
819
|
+
* const session = store.prefix("session:");
|
|
820
|
+
* await session.set("key1", "value1");
|
|
821
|
+
* console.log(await session.entries()); // session.
|
|
822
|
+
* // [["key1", "value1"]]
|
|
823
|
+
* console.log(await store.entries()); // store.
|
|
824
|
+
* // [["session:key1", "value1"]]
|
|
825
|
+
* ```
|
|
826
|
+
*
|
|
827
|
+
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
828
|
+
*/
|
|
829
|
+
prefix(prefix = "") {
|
|
830
|
+
const store = new _Store(
|
|
831
|
+
Promise.resolve(this.promise).then(() => this.client)
|
|
832
|
+
);
|
|
833
|
+
store.PREFIX = this.PREFIX + prefix;
|
|
834
|
+
return store;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Stop the connection to the store, if any:
|
|
838
|
+
*
|
|
839
|
+
* ```js
|
|
840
|
+
* await session.set("key1", "value1");
|
|
841
|
+
* await store.close();
|
|
842
|
+
* await session.set("key2", "value2"); // error
|
|
843
|
+
* ```
|
|
844
|
+
*
|
|
845
|
+
* **[→ Full .close() Docs](https://polystore.dev/documentation#close)**
|
|
846
|
+
*/
|
|
847
|
+
async close() {
|
|
848
|
+
await this.promise;
|
|
849
|
+
if (this.client.close) {
|
|
850
|
+
return this.client.close();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
function createStore(client) {
|
|
855
|
+
return new Store(client);
|
|
856
|
+
}
|
|
857
|
+
export {
|
|
858
|
+
createStore as default
|
|
859
|
+
};
|