polystore 0.7.0 → 0.9.0
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/assets/autocomplete.png +0 -0
- package/assets/autocomplete.webp +0 -0
- package/assets/favicon.png +0 -0
- package/assets/home.html +379 -0
- package/assets/splash.png +0 -0
- package/documentation.page.json +12 -0
- package/package.json +8 -3
- package/readme.md +297 -81
- package/src/clients/cloudflare.js +49 -0
- package/src/clients/cookie.js +55 -0
- package/src/clients/etcd.js +39 -0
- package/src/clients/file.js +75 -0
- package/src/clients/forage.js +42 -0
- package/src/clients/index.js +21 -0
- package/src/clients/level.js +45 -0
- package/src/clients/memory.js +38 -0
- package/src/clients/redis.js +56 -0
- package/src/clients/storage.js +43 -0
- package/src/index.js +269 -358
- package/src/index.test.js +314 -22
- package/src/index.types.ts +1 -0
- package/src/{index.d.ts → indexa.d.ts} +5 -3
- package/src/test/customFull.js +38 -0
- package/src/test/customSimple.js +23 -0
- package/src/test/setup.js +12 -0
- package/src/utils.js +44 -0
package/src/index.test.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
2
|
|
|
3
|
+
import { jest } from "@jest/globals";
|
|
4
|
+
import { EdgeKVNamespace as KVNamespace } from "edge-mock";
|
|
5
|
+
import { Etcd3 } from "etcd3";
|
|
6
|
+
import { Level } from "level";
|
|
3
7
|
import localForage from "localforage";
|
|
4
8
|
import { createClient } from "redis";
|
|
5
9
|
|
|
6
10
|
import kv from "./";
|
|
11
|
+
import customFull from "./test/customFull.js";
|
|
12
|
+
import customSimple from "./test/customSimple.js";
|
|
7
13
|
|
|
8
14
|
global.setImmediate = global.setImmediate || ((cb) => setTimeout(cb, 0));
|
|
9
15
|
|
|
@@ -15,17 +21,83 @@ stores.push(["kv(sessionStorage)", kv(sessionStorage)]);
|
|
|
15
21
|
stores.push(["kv(localForage)", kv(localForage)]);
|
|
16
22
|
const path = `file://${process.cwd()}/src/test/data.json`;
|
|
17
23
|
stores.push([`kv(new URL("${path}"))`, kv(new URL(path))]);
|
|
24
|
+
const path2 = `file://${process.cwd()}/src/test/data.json`;
|
|
25
|
+
stores.push([`kv("${path2}")`, kv(path2)]);
|
|
26
|
+
stores.push([`kv("cookie")`, kv("cookie")]);
|
|
27
|
+
stores.push(["kv(new KVNamespace())", kv(new KVNamespace())]);
|
|
28
|
+
stores.push([`kv(new Level("data"))`, kv(new Level("data"))]);
|
|
18
29
|
if (process.env.REDIS) {
|
|
19
30
|
stores.push(["kv(redis)", kv(createClient().connect())]);
|
|
20
31
|
}
|
|
21
|
-
|
|
32
|
+
if (process.env.ETCD) {
|
|
33
|
+
stores.push(["kv(new Etcd3())", kv(new Etcd3())]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
stores.push(["kv(customSimple)", kv(customSimple)]);
|
|
37
|
+
stores.push(["kv(customFull)", kv(customFull)]);
|
|
22
38
|
|
|
23
39
|
const delay = (t) => new Promise((done) => setTimeout(done, t));
|
|
24
40
|
|
|
41
|
+
class Base {
|
|
42
|
+
get() {}
|
|
43
|
+
set() {}
|
|
44
|
+
entries() {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
global.console = {
|
|
48
|
+
...console,
|
|
49
|
+
warn: jest.fn(),
|
|
50
|
+
};
|
|
51
|
+
|
|
25
52
|
describe("potato", () => {
|
|
26
53
|
it("a potato is not a valid store", async () => {
|
|
27
54
|
await expect(() => kv("potato").get("any")).rejects.toThrow();
|
|
28
55
|
});
|
|
56
|
+
|
|
57
|
+
it("an empty object is not a valid store", async () => {
|
|
58
|
+
await expect(() => kv({}).get("any")).rejects.toThrow({
|
|
59
|
+
message: "A client should have at least a .get(), .set() and .entries()",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("cannot handle no EXPIRES + keys", async () => {
|
|
64
|
+
await expect(() =>
|
|
65
|
+
kv(
|
|
66
|
+
class extends Base {
|
|
67
|
+
has() {}
|
|
68
|
+
}
|
|
69
|
+
).get("any")
|
|
70
|
+
).rejects.toThrow({
|
|
71
|
+
message:
|
|
72
|
+
"You can only define client.has() when the client manages the expiration; otherwise please do NOT define .has() and let us manage it",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("cannot handle no EXPIRES + keys", async () => {
|
|
77
|
+
await expect(() =>
|
|
78
|
+
kv(
|
|
79
|
+
class extends Base {
|
|
80
|
+
keys() {}
|
|
81
|
+
}
|
|
82
|
+
).get("any")
|
|
83
|
+
).rejects.toThrow({
|
|
84
|
+
message:
|
|
85
|
+
"You can only define client.keys() when the client manages the expiration; otherwise please do NOT define .keys() and let us manage them",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("warns the user with no EXPIRES + .values()", async () => {
|
|
90
|
+
const warn = jest
|
|
91
|
+
.spyOn(console, "warn")
|
|
92
|
+
.mockImplementationOnce(() => "Hello");
|
|
93
|
+
await kv(
|
|
94
|
+
class extends Base {
|
|
95
|
+
values() {}
|
|
96
|
+
}
|
|
97
|
+
).get("any");
|
|
98
|
+
expect(warn).toHaveBeenCalled(); // But at least we warn them
|
|
99
|
+
warn.mockClear();
|
|
100
|
+
});
|
|
29
101
|
});
|
|
30
102
|
|
|
31
103
|
for (let [name, store] of stores) {
|
|
@@ -36,9 +108,15 @@ for (let [name, store] of stores) {
|
|
|
36
108
|
|
|
37
109
|
afterAll(async () => {
|
|
38
110
|
await store.clear();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
111
|
+
await store.close();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("can perform a CRUD", async () => {
|
|
115
|
+
expect(await store.get("a")).toBe(null);
|
|
116
|
+
expect(await store.set("a", "b")).toBe("a");
|
|
117
|
+
expect(await store.get("a")).toBe("b");
|
|
118
|
+
expect(await store.del("a")).toBe("a");
|
|
119
|
+
expect(await store.get("a")).toBe(null);
|
|
42
120
|
});
|
|
43
121
|
|
|
44
122
|
it("is empty on the start", async () => {
|
|
@@ -62,6 +140,34 @@ for (let [name, store] of stores) {
|
|
|
62
140
|
expect(key).toBe("a");
|
|
63
141
|
});
|
|
64
142
|
|
|
143
|
+
it("can store values with a semicolon", async () => {
|
|
144
|
+
const key = await store.set("a", "b;c");
|
|
145
|
+
expect(await store.get("a")).toBe("b;c");
|
|
146
|
+
expect(await store.has("a")).toBe(true);
|
|
147
|
+
expect(key).toBe("a");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("can store values with an equal", async () => {
|
|
151
|
+
const key = await store.set("a", "b=c");
|
|
152
|
+
expect(await store.get("a")).toBe("b=c");
|
|
153
|
+
expect(await store.has("a")).toBe(true);
|
|
154
|
+
expect(key).toBe("a");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("can store values with a semicolon in the key", async () => {
|
|
158
|
+
const key = await store.set("a;b", "c");
|
|
159
|
+
expect(await store.get("a;b")).toBe("c");
|
|
160
|
+
expect(await store.has("a;b")).toBe(true);
|
|
161
|
+
expect(key).toBe("a;b");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("can store values with an equal in the key", async () => {
|
|
165
|
+
const key = await store.set("a=b", "c");
|
|
166
|
+
expect(await store.get("a=b")).toBe("c");
|
|
167
|
+
expect(await store.has("a=b")).toBe(true);
|
|
168
|
+
expect(key).toBe("a=b");
|
|
169
|
+
});
|
|
170
|
+
|
|
65
171
|
it("can store basic types", async () => {
|
|
66
172
|
await store.set("a", 10);
|
|
67
173
|
expect(await store.get("a")).toEqual(10);
|
|
@@ -118,7 +224,8 @@ for (let [name, store] of stores) {
|
|
|
118
224
|
await store.set("a:1", "a1");
|
|
119
225
|
await store.set("b:0", "b0");
|
|
120
226
|
await store.set("a:2", "a2");
|
|
121
|
-
expect((await store.keys(
|
|
227
|
+
expect((await store.keys()).sort()).toEqual(["a:0", "a:1", "a:2", "b:0"]);
|
|
228
|
+
expect((await store.prefix("a:").keys()).sort()).toEqual(["0", "1", "2"]);
|
|
122
229
|
});
|
|
123
230
|
|
|
124
231
|
it("can get the values with a colon prefix", async () => {
|
|
@@ -126,7 +233,11 @@ for (let [name, store] of stores) {
|
|
|
126
233
|
await store.set("a:1", "a1");
|
|
127
234
|
await store.set("b:0", "b0");
|
|
128
235
|
await store.set("a:2", "a2");
|
|
129
|
-
expect((await store.
|
|
236
|
+
expect((await store.prefix("a:").values()).sort()).toEqual([
|
|
237
|
+
"a0",
|
|
238
|
+
"a1",
|
|
239
|
+
"a2",
|
|
240
|
+
]);
|
|
130
241
|
});
|
|
131
242
|
|
|
132
243
|
it("can get the entries with a colon prefix", async () => {
|
|
@@ -134,10 +245,16 @@ for (let [name, store] of stores) {
|
|
|
134
245
|
await store.set("a:1", "a1");
|
|
135
246
|
await store.set("b:0", "b0");
|
|
136
247
|
await store.set("a:2", "a2");
|
|
137
|
-
expect((await store.entries(
|
|
248
|
+
expect((await store.entries()).sort()).toEqual([
|
|
138
249
|
["a:0", "a0"],
|
|
139
250
|
["a:1", "a1"],
|
|
140
251
|
["a:2", "a2"],
|
|
252
|
+
["b:0", "b0"],
|
|
253
|
+
]);
|
|
254
|
+
expect((await store.prefix("a:").entries()).sort()).toEqual([
|
|
255
|
+
["0", "a0"],
|
|
256
|
+
["1", "a1"],
|
|
257
|
+
["2", "a2"],
|
|
141
258
|
]);
|
|
142
259
|
});
|
|
143
260
|
|
|
@@ -146,10 +263,10 @@ for (let [name, store] of stores) {
|
|
|
146
263
|
await store.set("a:1", "a1");
|
|
147
264
|
await store.set("b:0", "b0");
|
|
148
265
|
await store.set("a:2", "a2");
|
|
149
|
-
expect(await store.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
266
|
+
expect(await store.prefix("a:").all()).toEqual({
|
|
267
|
+
0: "a0",
|
|
268
|
+
1: "a1",
|
|
269
|
+
2: "a2",
|
|
153
270
|
});
|
|
154
271
|
});
|
|
155
272
|
|
|
@@ -158,7 +275,8 @@ for (let [name, store] of stores) {
|
|
|
158
275
|
await store.set("a-1", "a1");
|
|
159
276
|
await store.set("b-0", "b0");
|
|
160
277
|
await store.set("a-2", "a2");
|
|
161
|
-
expect((await store.keys(
|
|
278
|
+
expect((await store.keys()).sort()).toEqual(["a-0", "a-1", "a-2", "b-0"]);
|
|
279
|
+
expect((await store.prefix("a-").keys()).sort()).toEqual(["0", "1", "2"]);
|
|
162
280
|
});
|
|
163
281
|
|
|
164
282
|
it("can get the values with a dash prefix", async () => {
|
|
@@ -166,7 +284,11 @@ for (let [name, store] of stores) {
|
|
|
166
284
|
await store.set("a-1", "a1");
|
|
167
285
|
await store.set("b-0", "b0");
|
|
168
286
|
await store.set("a-2", "a2");
|
|
169
|
-
expect((await store.
|
|
287
|
+
expect((await store.prefix("a-").values()).sort()).toEqual([
|
|
288
|
+
"a0",
|
|
289
|
+
"a1",
|
|
290
|
+
"a2",
|
|
291
|
+
]);
|
|
170
292
|
});
|
|
171
293
|
|
|
172
294
|
it("can get the entries with a dash prefix", async () => {
|
|
@@ -174,10 +296,10 @@ for (let [name, store] of stores) {
|
|
|
174
296
|
await store.set("a-1", "a1");
|
|
175
297
|
await store.set("b-0", "b0");
|
|
176
298
|
await store.set("a-2", "a2");
|
|
177
|
-
expect((await store.
|
|
178
|
-
["
|
|
179
|
-
["
|
|
180
|
-
["
|
|
299
|
+
expect((await store.prefix("a-").entries()).sort()).toEqual([
|
|
300
|
+
["0", "a0"],
|
|
301
|
+
["1", "a1"],
|
|
302
|
+
["2", "a2"],
|
|
181
303
|
]);
|
|
182
304
|
});
|
|
183
305
|
|
|
@@ -186,10 +308,10 @@ for (let [name, store] of stores) {
|
|
|
186
308
|
await store.set("a-1", "a1");
|
|
187
309
|
await store.set("b-0", "b0");
|
|
188
310
|
await store.set("a-2", "a2");
|
|
189
|
-
expect(await store.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
311
|
+
expect(await store.prefix("a-").all()).toEqual({
|
|
312
|
+
0: "a0",
|
|
313
|
+
1: "a1",
|
|
314
|
+
2: "a2",
|
|
193
315
|
});
|
|
194
316
|
});
|
|
195
317
|
|
|
@@ -198,6 +320,15 @@ for (let [name, store] of stores) {
|
|
|
198
320
|
expect(await store.get("a")).toBe("b");
|
|
199
321
|
await store.del("a");
|
|
200
322
|
expect(await store.get("a")).toBe(null);
|
|
323
|
+
expect(await store.keys()).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("can delete the data by setting it to null", async () => {
|
|
327
|
+
await store.set("a", "b");
|
|
328
|
+
expect(await store.get("a")).toBe("b");
|
|
329
|
+
await store.set("a", null);
|
|
330
|
+
expect(await store.get("a")).toBe(null);
|
|
331
|
+
expect(await store.keys()).toEqual([]);
|
|
201
332
|
});
|
|
202
333
|
|
|
203
334
|
it("can clear all the values", async () => {
|
|
@@ -211,6 +342,13 @@ for (let [name, store] of stores) {
|
|
|
211
342
|
});
|
|
212
343
|
|
|
213
344
|
describe("expires", () => {
|
|
345
|
+
// The mock implementation does NOT support expiration 😪
|
|
346
|
+
if (name === "kv(new KVNamespace())") return;
|
|
347
|
+
|
|
348
|
+
afterEach(() => {
|
|
349
|
+
jest.restoreAllMocks();
|
|
350
|
+
});
|
|
351
|
+
|
|
214
352
|
it("expires = 0 means immediately", async () => {
|
|
215
353
|
await store.set("a", "b", { expires: 0 });
|
|
216
354
|
expect(await store.get("a")).toBe(null);
|
|
@@ -248,49 +386,203 @@ for (let [name, store] of stores) {
|
|
|
248
386
|
expect(await store.get("a")).toBe("b");
|
|
249
387
|
});
|
|
250
388
|
|
|
251
|
-
if (
|
|
389
|
+
if (
|
|
390
|
+
name !== `kv("cookie")` &&
|
|
391
|
+
name !== `kv(redis)` &&
|
|
392
|
+
name !== `kv(new Etcd3())`
|
|
393
|
+
) {
|
|
252
394
|
it("can use 0.01 expire", async () => {
|
|
253
395
|
// 10ms
|
|
254
396
|
await store.set("a", "b", { expires: 0.01 });
|
|
397
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
398
|
+
expect(await store.values()).toEqual(["b"]);
|
|
255
399
|
expect(await store.get("a")).toBe("b");
|
|
256
400
|
await delay(100);
|
|
401
|
+
expect(await store.keys()).toEqual([]);
|
|
402
|
+
expect(await store.values()).toEqual([]);
|
|
257
403
|
expect(await store.get("a")).toBe(null);
|
|
258
404
|
});
|
|
259
405
|
|
|
260
406
|
it("can use 0.01s expire", async () => {
|
|
261
407
|
await store.set("a", "b", { expires: "0.01s" });
|
|
408
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
409
|
+
expect(await store.values()).toEqual(["b"]);
|
|
262
410
|
expect(await store.get("a")).toBe("b");
|
|
263
411
|
await delay(100);
|
|
412
|
+
expect(await store.keys()).toEqual([]);
|
|
413
|
+
expect(await store.values()).toEqual([]);
|
|
264
414
|
expect(await store.get("a")).toBe(null);
|
|
265
415
|
});
|
|
266
416
|
|
|
267
417
|
it("can use 0.01seconds expire", async () => {
|
|
268
418
|
await store.set("a", "b", { expires: "0.01seconds" });
|
|
419
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
420
|
+
expect(await store.values()).toEqual(["b"]);
|
|
269
421
|
expect(await store.get("a")).toBe("b");
|
|
270
422
|
await delay(100);
|
|
423
|
+
expect(await store.keys()).toEqual([]);
|
|
424
|
+
expect(await store.values()).toEqual([]);
|
|
271
425
|
expect(await store.get("a")).toBe(null);
|
|
272
426
|
});
|
|
273
427
|
|
|
274
428
|
it("can use 10ms expire", async () => {
|
|
275
429
|
await store.set("a", "b", { expires: "10ms" });
|
|
430
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
431
|
+
expect(await store.values()).toEqual(["b"]);
|
|
276
432
|
expect(await store.get("a")).toBe("b");
|
|
277
433
|
await delay(100);
|
|
434
|
+
expect(await store.keys()).toEqual([]);
|
|
435
|
+
expect(await store.values()).toEqual([]);
|
|
278
436
|
expect(await store.get("a")).toBe(null);
|
|
279
437
|
});
|
|
438
|
+
|
|
439
|
+
it("removes the expired key with .get()", async () => {
|
|
440
|
+
await store.set("a", "b", { expires: "10ms" });
|
|
441
|
+
const spy = jest.spyOn(store, "del");
|
|
442
|
+
expect(spy).not.toHaveBeenCalled();
|
|
443
|
+
await delay(100);
|
|
444
|
+
expect(spy).not.toHaveBeenCalled(); // Nothing we can do 😪
|
|
445
|
+
expect(await store.get("a")).toBe(null);
|
|
446
|
+
expect(spy).toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("removes the expired key with .keys()", async () => {
|
|
450
|
+
await store.set("a", "b", { expires: "10ms" });
|
|
451
|
+
const spy = jest.spyOn(store, "del");
|
|
452
|
+
expect(spy).not.toHaveBeenCalled();
|
|
453
|
+
await delay(100);
|
|
454
|
+
expect(spy).not.toHaveBeenCalled(); // Nothing we can do 😪
|
|
455
|
+
expect(await store.keys()).toEqual([]);
|
|
456
|
+
expect(spy).toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("CANNOT remove the expired key with .values()", async () => {
|
|
460
|
+
await store.set("a", "b", { expires: "10ms" });
|
|
461
|
+
const spy = jest.spyOn(store, "del");
|
|
462
|
+
expect(spy).not.toHaveBeenCalled();
|
|
463
|
+
await delay(100);
|
|
464
|
+
expect(spy).not.toHaveBeenCalled(); // Nothing we can do 😪
|
|
465
|
+
expect(await store.values()).toEqual([]);
|
|
466
|
+
if (!store.client.EXPIRES && store.client.values) {
|
|
467
|
+
expect(spy).not.toHaveBeenCalled(); // Nothing we can do 😪😪
|
|
468
|
+
} else {
|
|
469
|
+
expect(spy).toHaveBeenCalled();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
280
472
|
} else {
|
|
281
473
|
it("can use 1 (second) expire", async () => {
|
|
282
474
|
await store.set("a", "b", { expires: 1 });
|
|
475
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
476
|
+
expect(await store.values()).toEqual(["b"]);
|
|
283
477
|
expect(await store.get("a")).toBe("b");
|
|
284
478
|
await delay(2000);
|
|
479
|
+
expect(await store.keys()).toEqual([]);
|
|
480
|
+
expect(await store.values()).toEqual([]);
|
|
285
481
|
expect(await store.get("a")).toBe(null);
|
|
286
482
|
});
|
|
287
483
|
it("can use 1s expire", async () => {
|
|
288
484
|
await store.set("a", "b", { expires: "1s" });
|
|
485
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
486
|
+
expect(await store.values()).toEqual(["b"]);
|
|
289
487
|
expect(await store.get("a")).toBe("b");
|
|
290
488
|
await delay(2000);
|
|
489
|
+
expect(await store.keys()).toEqual([]);
|
|
490
|
+
expect(await store.values()).toEqual([]);
|
|
291
491
|
expect(await store.get("a")).toBe(null);
|
|
292
492
|
});
|
|
293
493
|
}
|
|
294
494
|
});
|
|
495
|
+
|
|
496
|
+
describe(".prefix()", () => {
|
|
497
|
+
let session;
|
|
498
|
+
beforeAll(() => {
|
|
499
|
+
session = store.prefix("session:");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("has the same methods", () => {
|
|
503
|
+
expect(Object.keys(store)).toEqual(Object.keys(session));
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("can write/read one", async () => {
|
|
507
|
+
const id = await session.set("a", "b");
|
|
508
|
+
expect(id).toBe("a");
|
|
509
|
+
expect(await session.get("a")).toBe("b");
|
|
510
|
+
expect(await store.get("session:a")).toBe("b");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("can add with the prefix", async () => {
|
|
514
|
+
const id = await session.add("b");
|
|
515
|
+
expect(id.length).toBe(24);
|
|
516
|
+
expect(id).not.toMatch(/^session\:/);
|
|
517
|
+
|
|
518
|
+
const keys = await store.keys();
|
|
519
|
+
expect(keys[0]).toMatch(/^session\:/);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("the group operations return the proper values", async () => {
|
|
523
|
+
await session.set("a", "b");
|
|
524
|
+
|
|
525
|
+
expect(await session.keys()).toEqual(["a"]);
|
|
526
|
+
expect(await session.values()).toEqual(["b"]);
|
|
527
|
+
expect(await session.entries()).toEqual([["a", "b"]]);
|
|
528
|
+
|
|
529
|
+
expect(await store.keys()).toEqual(["session:a"]);
|
|
530
|
+
expect(await store.values()).toEqual(["b"]);
|
|
531
|
+
expect(await store.entries()).toEqual([["session:a", "b"]]);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("clears only the substore", async () => {
|
|
535
|
+
await store.set("a", "b");
|
|
536
|
+
await session.set("c", "d");
|
|
537
|
+
|
|
538
|
+
expect((await store.keys()).sort()).toEqual(["a", "session:c"]);
|
|
539
|
+
await session.clear();
|
|
540
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe(".prefix().prefix()", () => {
|
|
545
|
+
let auth;
|
|
546
|
+
beforeAll(() => {
|
|
547
|
+
auth = store.prefix("session:").prefix("auth:");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("can write/read one", async () => {
|
|
551
|
+
const id = await auth.set("a", "b");
|
|
552
|
+
expect(id).toBe("a");
|
|
553
|
+
expect(await auth.get("a")).toBe("b");
|
|
554
|
+
expect(await store.get("session:auth:a")).toBe("b");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("can add with the prefix", async () => {
|
|
558
|
+
const id = await auth.add("b");
|
|
559
|
+
expect(id.length).toBe(24);
|
|
560
|
+
expect(id).not.toMatch(/^session\:/);
|
|
561
|
+
|
|
562
|
+
const keys = await store.keys();
|
|
563
|
+
expect(keys[0]).toMatch(/^session\:auth\:/);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("the group operations return the proper values", async () => {
|
|
567
|
+
await auth.set("a", "b");
|
|
568
|
+
|
|
569
|
+
expect(await auth.keys()).toEqual(["a"]);
|
|
570
|
+
expect(await auth.values()).toEqual(["b"]);
|
|
571
|
+
expect(await auth.entries()).toEqual([["a", "b"]]);
|
|
572
|
+
|
|
573
|
+
expect(await store.keys()).toEqual(["session:auth:a"]);
|
|
574
|
+
expect(await store.values()).toEqual(["b"]);
|
|
575
|
+
expect(await store.entries()).toEqual([["session:auth:a", "b"]]);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("clears only the substore", async () => {
|
|
579
|
+
await store.set("a", "b");
|
|
580
|
+
await auth.set("c", "d");
|
|
581
|
+
|
|
582
|
+
expect((await store.keys()).sort()).toEqual(["a", "session:auth:c"]);
|
|
583
|
+
await auth.clear();
|
|
584
|
+
expect(await store.keys()).toEqual(["a"]);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
295
587
|
});
|
|
296
588
|
}
|
package/src/index.types.ts
CHANGED
|
@@ -8,6 +8,7 @@ const store = kv();
|
|
|
8
8
|
await store.set("key", "value", {});
|
|
9
9
|
await store.set("key", "value", { expires: 100 });
|
|
10
10
|
await store.set("key", "value", { expires: "100s" });
|
|
11
|
+
await store.prefix("a:").prefix("b:").get("hello");
|
|
11
12
|
const key1: string = await store.add("value");
|
|
12
13
|
const key2: string = await store.add("value", { expires: 100 });
|
|
13
14
|
const key3: string = await store.add("value", { expires: "100s" });
|
|
@@ -8,9 +8,11 @@ type Store = {
|
|
|
8
8
|
has: (key: Key) => Promise<boolean>;
|
|
9
9
|
del: (key: Key) => Promise<null>;
|
|
10
10
|
|
|
11
|
-
keys: (
|
|
12
|
-
values: (
|
|
13
|
-
entries: (
|
|
11
|
+
keys: () => Promise<string[]>;
|
|
12
|
+
values: () => Promise<any[]>;
|
|
13
|
+
entries: () => Promise<[key: string, value: any][]>;
|
|
14
|
+
|
|
15
|
+
prefix: (prefix: string) => Store;
|
|
14
16
|
|
|
15
17
|
clear: () => Promise<null>;
|
|
16
18
|
close?: () => Promise<null>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const dataSource = {};
|
|
2
|
+
|
|
3
|
+
export default class MyClient {
|
|
4
|
+
get(key) {
|
|
5
|
+
return dataSource[key];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
set(key, value) {
|
|
9
|
+
dataSource[key] = value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
add(prefix, value) {
|
|
13
|
+
const id = Math.random().toString(16).slice(2).padStart(24, "0");
|
|
14
|
+
this.set(prefix + id, value);
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
del(key) {
|
|
19
|
+
delete dataSource[key];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Filter them by the prefix, note that `prefix` will always be a string
|
|
23
|
+
entries(prefix) {
|
|
24
|
+
const entries = Object.entries(dataSource);
|
|
25
|
+
if (!prefix) return entries;
|
|
26
|
+
return entries.filter(([key]) => key.startsWith(prefix));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
values(prefix) {
|
|
30
|
+
const list = this.entries(prefix);
|
|
31
|
+
return list.map((e) => e[1]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cannot have a keys() if it's an unamanaged store
|
|
35
|
+
// keys(prefix) {
|
|
36
|
+
// // This should throw
|
|
37
|
+
// }
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const dataSource = {};
|
|
2
|
+
|
|
3
|
+
export default class MyClient {
|
|
4
|
+
get(key) {
|
|
5
|
+
return dataSource[key];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// No need to stringify it or anything for a plain object storage
|
|
9
|
+
set(key, value) {
|
|
10
|
+
if (value === null) {
|
|
11
|
+
delete dataSource[key];
|
|
12
|
+
} else {
|
|
13
|
+
dataSource[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Filter them by the prefix, note that `prefix` will always be a string
|
|
18
|
+
entries(prefix) {
|
|
19
|
+
const entries = Object.entries(dataSource);
|
|
20
|
+
if (!prefix) return entries;
|
|
21
|
+
return entries.filter(([key, value]) => key.startsWith(prefix));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as util from "util";
|
|
2
|
+
|
|
3
|
+
// ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
|
4
|
+
// ref: https://github.com/jsdom/jsdom/issues/2524
|
|
5
|
+
Object.defineProperty(window, "TextEncoder", {
|
|
6
|
+
writable: true,
|
|
7
|
+
value: util.TextEncoder,
|
|
8
|
+
});
|
|
9
|
+
Object.defineProperty(window, "TextDecoder", {
|
|
10
|
+
writable: true,
|
|
11
|
+
value: util.TextDecoder,
|
|
12
|
+
});
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
|
|
2
|
+
|
|
3
|
+
parse.millisecond = parse.ms = 0.001;
|
|
4
|
+
parse.second = parse.sec = parse.s = parse[""] = 1;
|
|
5
|
+
parse.minute = parse.min = parse.m = parse.s * 60;
|
|
6
|
+
parse.hour = parse.hr = parse.h = parse.m * 60;
|
|
7
|
+
parse.day = parse.d = parse.h * 24;
|
|
8
|
+
parse.week = parse.wk = parse.w = parse.d * 7;
|
|
9
|
+
parse.year = parse.yr = parse.y = parse.d * 365.25;
|
|
10
|
+
parse.month = parse.b = parse.y / 12;
|
|
11
|
+
|
|
12
|
+
// Returns the time in milliseconds
|
|
13
|
+
function parse(str) {
|
|
14
|
+
if (str === null || str === undefined) return null;
|
|
15
|
+
if (typeof str === "number") return str;
|
|
16
|
+
// ignore commas/placeholders
|
|
17
|
+
str = str.toLowerCase().replace(/[,_]/g, "");
|
|
18
|
+
let [_, value, units] = times.exec(str) || [];
|
|
19
|
+
if (!units) return null;
|
|
20
|
+
const unitValue = parse[units] || parse[units.replace(/s$/, "")];
|
|
21
|
+
if (!unitValue) return null;
|
|
22
|
+
const result = unitValue * parseFloat(value, 10);
|
|
23
|
+
return Math.abs(Math.round(result * 1000) / 1000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// "nanoid" imported manually
|
|
27
|
+
// Something about improved GZIP performance with this string
|
|
28
|
+
const urlAlphabet =
|
|
29
|
+
"useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
30
|
+
|
|
31
|
+
function createId() {
|
|
32
|
+
let size = 24;
|
|
33
|
+
let id = "";
|
|
34
|
+
let bytes = crypto.getRandomValues(new Uint8Array(size));
|
|
35
|
+
while (size--) {
|
|
36
|
+
// Using the bitwise AND operator to "cap" the value of
|
|
37
|
+
// the random byte from 255 to 63, in that way we can make sure
|
|
38
|
+
// that the value will be a valid index for the "chars" string.
|
|
39
|
+
id += urlAlphabet[bytes[size] & 61];
|
|
40
|
+
}
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { parse, createId };
|