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/src/index.js CHANGED
@@ -1,407 +1,318 @@
1
- const layers = {};
2
-
3
- const times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
4
-
5
- parse.millisecond = parse.ms = 0.001;
6
- parse.second = parse.sec = parse.s = parse[""] = 1;
7
- parse.minute = parse.min = parse.m = parse.s * 60;
8
- parse.hour = parse.hr = parse.h = parse.m * 60;
9
- parse.day = parse.d = parse.h * 24;
10
- parse.week = parse.wk = parse.w = parse.d * 7;
11
- parse.year = parse.yr = parse.y = parse.d * 365.25;
12
- parse.month = parse.b = parse.y / 12;
13
-
14
- // Returns the time in milliseconds
15
- function parse(str) {
16
- if (str === null || str === undefined) return null;
17
- if (typeof str === "number") return str;
18
- // ignore commas/placeholders
19
- str = str.toLowerCase().replace(/[,_]/g, "");
20
- let [_, value, units] = times.exec(str) || [];
21
- if (!units) return null;
22
- const unitValue = parse[units] || parse[units.replace(/s$/, "")];
23
- if (!unitValue) return null;
24
- const result = unitValue * parseFloat(value, 10);
25
- return Math.abs(Math.round(result * 1000) / 1000);
1
+ import clients from "./clients/index.js";
2
+ import { createId, parse } from "./utils.js";
3
+
4
+ function isClass(func) {
5
+ return (
6
+ typeof func === "function" &&
7
+ /^class\s/.test(Function.prototype.toString.call(func))
8
+ );
26
9
  }
27
10
 
28
- // "nanoid" imported manually
29
- // Something about improved GZIP performance with this string
30
- const urlAlphabet =
31
- "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
32
-
33
- export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
34
-
35
- function generateId() {
36
- let size = 24;
37
- let id = "";
38
- let bytes = crypto.getRandomValues(new Uint8Array(size));
39
- while (size--) {
40
- // Using the bitwise AND operator to "cap" the value of
41
- // the random byte from 255 to 63, in that way we can make sure
42
- // that the value will be a valid index for the "chars" string.
43
- id += urlAlphabet[bytes[size] & 61];
11
+ const getClient = (store) => {
12
+ // Already a fully compliant KV store
13
+ if (store instanceof Store) return store.client;
14
+
15
+ // One of the supported ones, so we receive an instance and
16
+ // wrap it with the client wrapper
17
+ for (let client of Object.values(clients)) {
18
+ if (client.test && client.test(store)) {
19
+ return new client(store);
20
+ }
44
21
  }
45
- return id;
46
- }
47
22
 
48
- layers.extra = (store) => {
49
- const add = async (value, options) => store.set(generateId(), value, options);
50
- const has = async (key) => (await store.get(key)) !== null;
51
- const del = async (key) => store.set(key, null);
52
- const all = async (prefix = "") => {
53
- const entries = await store.entries(prefix);
54
- return Object.fromEntries(entries);
55
- };
56
- const keys = async (prefix = "") => {
57
- const all = await store.entries(prefix);
58
- return all.map((p) => p[0]);
59
- };
60
- const values = async (prefix = "") => {
61
- const all = await store.entries(prefix);
62
- return all.map((p) => p[1]);
63
- };
64
- return { add, has, del, keys, values, all, ...store };
23
+ // A raw one, we just receive the single instance to use directly
24
+ if (isClass(store)) {
25
+ return new store();
26
+ }
27
+ return store;
65
28
  };
66
29
 
67
- // Adds an expiration layer to those stores that don't have it;
68
- // it's not perfect since it's not deleted until it's read, but
69
- // hey it's better than nothing
70
- layers.expire = (store) => {
71
- // Item methods
72
- const get = async (key) => {
73
- const data = await store.get(key);
74
- if (!data) return null;
75
- const { value, expire } = data;
76
- // It never expires
77
- if (expire === null) return value;
78
- const diff = expire - new Date().getTime();
79
- if (diff <= 0) return null;
80
- return value;
81
- };
82
- const set = async (key, value, { expire, expires } = {}) => {
83
- const time = parse(expire || expires);
84
- // Already expired, or do _not_ save it, then delete it
85
- if (value === null || time === 0) return store.set(key, null);
86
- const expDiff = time !== null ? new Date().getTime() + time * 1000 : null;
87
- return store.set(key, { expire: expDiff, value });
88
- };
89
-
90
- // Group methods
91
- const entries = async (prefix = "") => {
92
- const all = await store.entries(prefix);
93
- const now = new Date().getTime();
94
- return all
95
- .filter(([, data]) => {
96
- // There's no data, so remove this
97
- if (!data || data === null) return false;
30
+ class Store {
31
+ PREFIX = "";
98
32
 
99
- // It never expires, so keep it
100
- const { expire } = data;
101
- if (expire === null) return true;
33
+ constructor(clientPromise = new Map()) {
34
+ this.promise = Promise.resolve(clientPromise).then(async (client) => {
35
+ this.client = getClient(client);
36
+ this.#validate(this.client);
37
+ if (this.client.open) {
38
+ await this.client.open();
39
+ }
40
+ if (this.client.connect) {
41
+ await this.client.connect();
42
+ }
43
+ this.promise = null;
44
+ return client;
45
+ });
46
+ }
102
47
 
103
- // It's expired, so remove it
104
- if (expire - now <= 0) return false;
48
+ #validate(client) {
49
+ if (!client.set || !client.get || !client.entries) {
50
+ throw new Error(
51
+ "A client should have at least a .get(), .set() and .entries()"
52
+ );
53
+ }
105
54
 
106
- // It's not expired, keep it
107
- return true;
108
- })
109
- .map(([key, data]) => [key, data.value]);
110
- };
55
+ if (!client.EXPIRES) {
56
+ if (client.has) {
57
+ throw new Error(
58
+ `You can only define client.has() when the client manages the expiration; otherwise please do NOT define .has() and let us manage it`
59
+ );
60
+ }
61
+ if (client.keys) {
62
+ throw new Error(
63
+ `You can only define client.keys() when the client manages the expiration; otherwise please do NOT define .keys() and let us manage them`
64
+ );
65
+ }
66
+ if (client.values) {
67
+ console.warn(
68
+ `Since this KV client does not manage expiration, it's better not to define client.values() since it doesn't allow us to evict expired keys`
69
+ );
70
+ }
71
+ }
72
+ }
111
73
 
112
- // We want to force overwrite here!
113
- return { ...store, get, set, entries };
114
- };
74
+ async add(data, options = {}) {
75
+ await this.promise;
76
+ const expires = parse(options.expire ?? options.expires);
115
77
 
116
- layers.memory = (store) => {
117
- // Item methods
118
- const get = async (key) => store.get(key) ?? null;
119
- const set = async (key, data) => {
120
- if (data === null) {
121
- await store.delete(key);
122
- } else {
123
- await store.set(key, data);
78
+ // Use the underlying one from the client if found
79
+ if (this.client.add) {
80
+ if (this.client.EXPIRES) {
81
+ return this.client.add(this.PREFIX, data, { expires });
82
+ }
83
+
84
+ // In the data we need the timestamp since we need it "absolute":
85
+ const now = new Date().getTime();
86
+ const expDiff = expires === null ? null : now + expires * 1000;
87
+ return this.client.add(this.PREFIX, { expires: expDiff, value: data });
124
88
  }
125
- return key;
126
- };
127
89
 
128
- // Group methods
129
- const entries = async (prefix = "") => {
130
- const entries = [...store.entries()];
131
- return entries.filter((p) => p[0].startsWith(prefix));
132
- };
133
- const clear = () => store.clear();
90
+ const id = createId();
91
+ await this.set(id, data, { expires });
92
+ return id; // The plain one without the prefix
93
+ }
134
94
 
135
- return { get, set, entries, clear };
136
- };
95
+ async set(id, data, options = {}) {
96
+ await this.promise;
97
+ const key = this.PREFIX + id;
98
+ const expires = parse(options.expire ?? options.expires);
137
99
 
138
- layers.storage = (store) => {
139
- // Item methods
140
- const get = async (key) => (store[key] ? JSON.parse(store[key]) : null);
141
- const set = async (key, data) => {
100
+ // Quick delete
142
101
  if (data === null) {
143
- await store.removeItem(key);
144
- } else {
145
- await store.setItem(key, JSON.stringify(data));
102
+ await this.del(key, null);
103
+ return id;
146
104
  }
147
- return key;
148
- };
149
-
150
- // Group methods
151
- const entries = async (prefix = "") => {
152
- const entries = Object.entries(store);
153
- return entries
154
- .map((p) => [p[0], p[1] ? JSON.parse(p[1]) : null])
155
- .filter((p) => p[0].startsWith(prefix));
156
- };
157
- const clear = () => store.clear();
158
-
159
- return { get, set, entries, clear };
160
- };
161
105
 
162
- // Cookies auto-expire, so we cannot do expiration checks manually
163
- layers.cookie = () => {
164
- const getAll = () => {
165
- const all = {};
166
- for (let entry of document.cookie
167
- .split(";")
168
- .map((k) => k.trim())
169
- .filter(Boolean)) {
170
- const [key, data] = entry.split("=");
171
- try {
172
- all[key.trim()] = JSON.parse(decodeURIComponent(data.trim()));
173
- } catch (error) {
174
- // no-op (some 3rd party can set cookies independently)
175
- }
106
+ // The client manages the expiration, so let it manage it
107
+ if (this.client.EXPIRES) {
108
+ await this.client.set(key, data, { expires });
109
+ return id;
176
110
  }
177
- return all;
178
- };
179
-
180
- const get = async (key) => getAll()[key] ?? null;
181
111
 
182
- const set = async (key, data, { expire, expires } = {}) => {
183
- if (data === null) {
184
- await set(key, "", { expire: -100 });
185
- } else {
186
- const time = parse(expire || expires);
187
- const now = new Date().getTime();
188
- // NOTE: 0 is already considered here!
189
- const expireStr =
190
- time !== null
191
- ? `; expires=${new Date(now + time * 1000).toUTCString()}`
192
- : "";
193
- const value = encodeURIComponent(JSON.stringify(data));
194
- document.cookie = key + "=" + value + expireStr;
112
+ // Already expired, then delete it
113
+ if (expires === 0) {
114
+ await this.del(id);
115
+ return id;
195
116
  }
196
- return key;
197
- };
198
117
 
199
- // Group methods
200
- const entries = async (prefix = "") => {
201
- const all = Object.entries(getAll());
202
- return all.filter((p) => p[0].startsWith(prefix));
203
- };
118
+ // In the data we need the timestamp since we need it "absolute":
119
+ const now = new Date().getTime();
120
+ const expDiff = expires === null ? null : now + expires * 1000;
121
+ await this.client.set(key, { expires: expDiff, value: data });
122
+ return id;
123
+ }
204
124
 
205
- const clear = async () => {
206
- const keys = Object.keys(getAll());
207
- await Promise.all(keys.map((key) => set(key, null)));
208
- };
125
+ /**
126
+ * Read a single value from the KV store:
127
+ *
128
+ * ```js
129
+ * const key = await store.set("key1", "value1");
130
+ * const value = await store.get("key1");
131
+ * // "value1"
132
+ * ```
133
+ *
134
+ * **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
135
+ * @param {(string)} key
136
+ * @returns {(any)} value
137
+ */
138
+ async get(key) {
139
+ await this.promise;
140
+ const id = this.PREFIX + key;
141
+
142
+ const data = (await this.client.get(id)) ?? null;
143
+
144
+ // No value; nothing to do/check
145
+ if (data === null) return null;
146
+
147
+ // The client already managed expiration and there's STILL some data,
148
+ // so we can assume it's the raw user data
149
+ if (this.client.EXPIRES) return data;
150
+
151
+ // Make sure that if there's no data by now, empty is returned
152
+ if (!data) return null;
209
153
 
210
- return { get, set, entries, clear };
211
- };
154
+ // We manage expiration manually, so we know it should have this structure
155
+ // TODO: ADD A CHECK HERE
156
+ const { expires, value } = data;
212
157
 
213
- // Plain 'redis' and not ioredis or similar
214
- layers.redis = (store) => {
215
- const get = async (key) => {
216
- const value = await store.get(key);
217
- if (!value) return null;
218
- return JSON.parse(value);
219
- };
220
- const set = async (key, value, { expire, expires } = {}) => {
221
- const time = parse(expire || expires);
222
- if (value === null || time === 0) return del(key);
223
- const EX = time ? Math.round(time) : undefined;
224
- await store.set(key, JSON.stringify(value), { EX });
225
- return key;
226
- };
227
- const has = async (key) => Boolean(await store.exists(key));
228
- const del = async (key) => store.del(key);
229
-
230
- const keys = async (prefix = "") => store.keys(prefix + "*");
231
- const entries = async (prefix = "") => {
232
- const keys = await store.keys(prefix + "*");
233
- const values = await Promise.all(keys.map((k) => get(k)));
234
- return keys.map((k, i) => [k, values[i]]);
235
- };
236
- const clear = async () => store.flushAll();
237
- const close = async () => store.quit();
238
-
239
- return { get, set, has, del, keys, entries, clear, close };
240
- };
158
+ // It never expires
159
+ if (expires === null) return value ?? null;
241
160
 
242
- layers.localForage = (store) => {
243
- const get = async (key) => store.getItem(key);
244
- const set = async (key, value) => {
245
- if (value === null) {
246
- await store.removeItem(key);
247
- } else {
248
- await store.setItem(key, value);
161
+ // Already expired! Return nothing, and remove the whole key
162
+ if (expires <= new Date().getTime()) {
163
+ await this.del(key);
164
+ return null;
249
165
  }
250
- return key;
251
- };
252
- const entries = async (prefix = "") => {
253
- const all = await store.keys();
254
- const keys = all.filter((k) => k.startsWith(prefix));
255
- const values = await Promise.all(keys.map((key) => store.getItem(key)));
256
- return keys.map((key, i) => [key, values[i]]);
257
- };
258
- const clear = async () => store.clear();
259
-
260
- return { get, set, entries, clear };
261
- };
262
166
 
263
- layers.cloudflare = (store) => {
264
- const get = async (key) => {
265
- const data = await store.get(key);
266
- if (!data) return null;
267
- return JSON.parse(data);
268
- };
269
- const set = async (key, value, { expire, expires } = {}) => {
270
- const time = parse(expire || expires);
271
- if (value === null || time === 0) return del(key);
272
- const client = await store;
273
- const expirationTtl = time ? Math.round(time) : undefined;
274
- client.put(key, JSON.stringify(value), { expirationTtl });
275
- return key;
276
- };
277
- const has = async (key) => Boolean(await store.get(key));
278
- const del = (key) => store.delete(key);
279
-
280
- // Group methods
281
- const keys = async (prefix = "") => {
282
- const raw = await store.list({ prefix });
283
- return raw.keys;
284
- };
285
- const entries = async (prefix = "") => {
286
- const all = await keys(prefix);
287
- const values = await Promise.all(all.map((k) => get(k)));
288
- return all.map((key, i) => [key, values[i]]);
289
- };
290
- const clear = () => {};
291
- return { get, set, has, del, entries, keys, clear };
292
- };
167
+ return value;
168
+ }
293
169
 
294
- layers.file = (file) => {
295
- const fsProm = (async () => {
296
- // For the bundler, it doesn't like it otherwise
297
- const lib = ["node:fs", "promises"].join("/");
298
- const fsp = await import(lib);
299
- // We want to make sure the file already exists, so attempt to
300
- // create it (but not OVERWRITE it, that's why the x flag) and
301
- // it fails if it already exists
302
- await fsp.writeFile(file.pathname, "{}", { flag: "wx" }).catch((err) => {
303
- if (err.code !== "EEXIST") throw err;
304
- });
305
- return fsp;
306
- })();
307
- const getContent = async () => {
308
- const fsp = await fsProm;
309
- const text = await fsp.readFile(file.pathname, "utf8");
310
- if (!text) return {};
311
- return JSON.parse(text);
312
- };
313
- const setContent = async (data) => {
314
- const fsp = await fsProm;
315
- await fsp.writeFile(file.pathname, JSON.stringify(data, null, 2));
316
- };
317
- const get = async (key) => {
318
- const data = await getContent();
319
- return data[key] ?? null;
320
- };
321
- const set = async (key, value) => {
322
- const data = await getContent();
323
- if (value === null) {
324
- delete data[key];
325
- } else {
326
- data[key] = value;
170
+ async has(id) {
171
+ await this.promise;
172
+ const key = this.PREFIX + id;
173
+
174
+ if (this.client.has) {
175
+ return this.client.has(key);
327
176
  }
328
- await setContent(data);
329
- return key;
330
- };
331
- const has = async (key) => (await get(key)) !== null;
332
- const del = async (key) => set(key, null);
333
-
334
- // Group methods
335
- const entries = async (prefix = "") => {
336
- const data = await getContent();
337
- return Object.entries(data).filter((p) => p[0].startsWith(prefix));
338
- };
339
- const clear = async () => {
340
- await setContent({});
341
- };
342
- return { get, set, has, del, entries, clear };
343
- };
344
177
 
345
- const getStore = async (store) => {
346
- // Convert it to the normalized kv, then add the expiry layer on top
347
- if (store instanceof Map) {
348
- return layers.extra(layers.expire(layers.memory(store)));
178
+ const value = await this.get(key);
179
+ return value !== null;
349
180
  }
350
181
 
351
- if (typeof localStorage !== "undefined" && store === localStorage) {
352
- return layers.extra(layers.expire(layers.storage(store)));
353
- }
182
+ async del(id) {
183
+ await this.promise;
184
+ const key = this.PREFIX + id;
185
+
186
+ if (this.client.del) {
187
+ await this.client.del(key);
188
+ return id;
189
+ }
354
190
 
355
- if (typeof sessionStorage !== "undefined" && store === sessionStorage) {
356
- return layers.extra(layers.expire(layers.storage(store)));
191
+ await this.client.set(key, null, { expires: 0 });
192
+ return id;
357
193
  }
358
194
 
359
- if (store === "cookie") {
360
- return layers.extra(layers.cookie());
195
+ async entries() {
196
+ await this.promise;
197
+
198
+ const entries = await this.client.entries(this.PREFIX);
199
+ const list = entries.map(([key, data]) => [
200
+ key.slice(this.PREFIX.length),
201
+ data,
202
+ ]);
203
+
204
+ // The client already manages the expiration, so we can assume
205
+ // that at this point, all entries are not-expired
206
+ if (this.client.EXPIRES) return list;
207
+
208
+ // We need to do manual expiration checking
209
+ const now = new Date().getTime();
210
+ return list
211
+ .filter(([key, data]) => {
212
+ // Should never happen
213
+ if (!data || data.value === null) return false;
214
+
215
+ // It never expires, so keep it
216
+ const { expires } = data;
217
+ if (expires === null) return true;
218
+
219
+ // It's expired, so remove it
220
+ if (expires <= now) {
221
+ this.del(key);
222
+ return false;
223
+ }
224
+
225
+ // It's not expired, keep it
226
+ return true;
227
+ })
228
+ .map(([key, data]) => [key, data.value]);
361
229
  }
362
230
 
363
- if (store.defineDriver && store.dropInstance && store.INDEXEDDB) {
364
- return layers.extra(layers.expire(layers.localForage(store)));
231
+ async values() {
232
+ await this.promise;
233
+
234
+ if (this.client.values) {
235
+ const list = this.client.values(this.PREFIX);
236
+ if (this.client.EXPIRES) return list;
237
+ const now = new Date().getTime();
238
+ return list
239
+ .filter((data) => {
240
+ // There's no data, so remove this
241
+ if (!data || data.value === null) return false;
242
+
243
+ // It never expires, so keep it
244
+ const { expires } = data;
245
+ if (expires === null) return true;
246
+
247
+ // It's expired, so remove it
248
+ // We cannot unfortunately evict it since we don't know the key!
249
+ if (expires <= now) return false;
250
+
251
+ // It's not expired, keep it
252
+ return true;
253
+ })
254
+ .map((data) => data.value);
255
+ }
256
+
257
+ const entries = await this.entries();
258
+ return entries.map((e) => e[1]);
365
259
  }
366
260
 
367
- if (store.protocol && store.protocol === "file:") {
368
- return layers.extra(layers.expire(layers.file(store)));
261
+ async keys() {
262
+ await this.promise;
263
+
264
+ if (this.client.keys) {
265
+ const list = await this.client.keys(this.PREFIX);
266
+ if (!this.PREFIX) return list;
267
+ return list.map((k) => k.slice(this.PREFIX.length));
268
+ }
269
+
270
+ const entries = await this.entries();
271
+ return entries.map((e) => e[0]);
369
272
  }
370
273
 
371
- if (store.pSubscribe && store.sSubscribe) {
372
- return layers.extra(layers.redis(store));
274
+ async all() {
275
+ await this.promise;
276
+
277
+ if (this.client.all) {
278
+ const obj = await this.client.all(this.PREFIX);
279
+ if (!this.PREFIX) return obj;
280
+ const all = {};
281
+ for (let key in obj) {
282
+ all[key.slice(this.PREFIX.length)] = obj[key];
283
+ }
284
+ return all;
285
+ }
286
+
287
+ const entries = await this.entries();
288
+ return Object.fromEntries(entries);
373
289
  }
374
290
 
375
- if (store?.constructor?.name === "KvNamespace") {
376
- return layers.extra(layers.cloudflare(store));
291
+ async clear() {
292
+ await this.promise;
293
+
294
+ if (this.client.clear) {
295
+ return this.client.clear(this.PREFIX);
296
+ }
297
+
298
+ const keys = await this.keys();
299
+ // Note: this gives trouble of concurrent deletes in the FS
300
+ return await Promise.all(keys.map((key) => this.del(key)));
377
301
  }
378
302
 
379
- // ¯\_(ツ)_/¯
380
- return null;
381
- };
303
+ prefix(prefix = "") {
304
+ const store = new Store(
305
+ Promise.resolve(this.promise).then((client) => client || this.client)
306
+ );
307
+ store.PREFIX = this.PREFIX + prefix;
308
+ return store;
309
+ }
382
310
 
383
- export default function compat(storeClient = new Map()) {
384
- return new Proxy(
385
- {},
386
- {
387
- get: (instance, key) => {
388
- return async (...args) => {
389
- // Only once, even if called twice in succession, since the
390
- // second time will go straight to the await
391
- if (!instance.store && !instance.promise) {
392
- instance.promise = getStore(await storeClient);
393
- }
394
- instance.store = await instance.promise;
395
- // Throw at the first chance when the store failed to init:
396
- if (!instance.store) {
397
- throw new Error("Store is not valid");
398
- }
399
- // The store.close() is the only one allowed to be called even
400
- // if it doesn't exist, since it's optional in some stores
401
- if (!instance.store[key] && key === "close") return null;
402
- return instance.store[key](...args);
403
- };
404
- },
311
+ async close() {
312
+ if (this.client.close) {
313
+ return this.client.close();
405
314
  }
406
- );
315
+ }
407
316
  }
317
+
318
+ export default kv = (client) => new Store(client);