@esportsplus/web-storage 0.3.5 → 0.5.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/build/index.js CHANGED
@@ -2,6 +2,15 @@ import { decrypt, encrypt } from '@esportsplus/utilities';
2
2
  import { DriverType } from './constants.js';
3
3
  import { IndexedDBDriver } from './drivers/indexeddb.js';
4
4
  import { LocalStorageDriver } from './drivers/localstorage.js';
5
+ import { MemoryDriver } from './drivers/memory.js';
6
+ import { SessionStorageDriver } from './drivers/sessionstorage.js';
7
+ const VERSION_KEY = '__version__';
8
+ function isEnvelope(value) {
9
+ return value !== null
10
+ && typeof value === 'object'
11
+ && '__e' in value
12
+ && '__v' in value;
13
+ }
5
14
  async function deserialize(value, secret) {
6
15
  if (value === undefined || value === null) {
7
16
  return undefined;
@@ -26,84 +35,267 @@ async function serialize(value, secret) {
26
35
  }
27
36
  return value;
28
37
  }
38
+ function notify(globals, listeners, key, newValue, oldValue) {
39
+ let set = listeners.get(key);
40
+ if (set) {
41
+ for (let cb of set) {
42
+ cb(newValue, oldValue);
43
+ }
44
+ }
45
+ for (let cb of globals) {
46
+ cb(key, newValue, oldValue);
47
+ }
48
+ }
49
+ function unwrap(value) {
50
+ if (isEnvelope(value)) {
51
+ return {
52
+ expired: Date.now() > value.__e,
53
+ hasTTL: true,
54
+ value: value.__v
55
+ };
56
+ }
57
+ return { expired: false, hasTTL: false, value: value };
58
+ }
59
+ async function migrate(driver, migrations, version) {
60
+ let raw = await driver.get(VERSION_KEY), stored = typeof raw === 'number' ? raw : 0;
61
+ if (stored >= version) {
62
+ return;
63
+ }
64
+ let keys = Object.keys(migrations).map(Number).filter((v) => v > stored && v <= version).sort((a, b) => a - b);
65
+ for (let i = 0, n = keys.length; i < n; i++) {
66
+ let all = await driver.all(), data = {};
67
+ for (let key in all) {
68
+ if (key !== VERSION_KEY) {
69
+ data[key] = all[key];
70
+ }
71
+ }
72
+ let transformed = await migrations[keys[i]]({ all: () => Promise.resolve(data) });
73
+ await driver.clear();
74
+ let entries = [];
75
+ for (let key in transformed) {
76
+ entries.push([key, transformed[key]]);
77
+ }
78
+ if (entries.length > 0) {
79
+ await driver.replace(entries);
80
+ }
81
+ }
82
+ await driver.set(VERSION_KEY, version);
83
+ }
29
84
  class Local {
30
85
  driver;
86
+ globals;
87
+ listeners;
88
+ ready;
31
89
  secret;
90
+ version;
32
91
  constructor(options, secret) {
92
+ this.globals = new Set();
93
+ this.listeners = new Map();
33
94
  this.secret = secret || null;
34
- let { name, version = 1 } = options;
95
+ let { migrations, name, version = 1 } = options;
96
+ this.version = version;
35
97
  if (options.driver === DriverType.LocalStorage) {
36
98
  this.driver = new LocalStorageDriver(name, version);
37
99
  }
100
+ else if (options.driver === DriverType.Memory) {
101
+ this.driver = new MemoryDriver(name, version);
102
+ }
103
+ else if (options.driver === DriverType.SessionStorage) {
104
+ this.driver = new SessionStorageDriver(name, version);
105
+ }
38
106
  else {
39
107
  this.driver = new IndexedDBDriver(name, version);
40
108
  }
109
+ if (migrations) {
110
+ this.ready = migrate(this.driver, migrations, version);
111
+ }
112
+ else {
113
+ this.ready = Promise.resolve();
114
+ }
41
115
  }
42
116
  async all() {
43
- let raw = await this.driver.all(), result = {};
117
+ await this.ready;
118
+ let expired = [], raw = await this.driver.all(), result = {};
44
119
  for (let key in raw) {
45
- let value = await deserialize(raw[key], this.secret);
46
- if (value !== undefined) {
47
- result[key] = value;
120
+ if (key === VERSION_KEY) {
121
+ continue;
122
+ }
123
+ let deserialized = await deserialize(raw[key], this.secret), unwrapped = unwrap(deserialized);
124
+ if (deserialized === undefined) {
125
+ continue;
126
+ }
127
+ if (unwrapped.expired) {
128
+ expired.push(key);
129
+ continue;
48
130
  }
131
+ result[key] = unwrapped.value;
132
+ }
133
+ if (expired.length > 0) {
134
+ this.driver.delete(expired);
49
135
  }
50
136
  return result;
51
137
  }
52
138
  async clear() {
53
- return this.driver.clear();
139
+ await this.ready;
140
+ let allData = await this.all(), keys = Object.keys(allData);
141
+ await this.driver.clear();
142
+ await this.driver.set(VERSION_KEY, this.version);
143
+ for (let i = 0, n = keys.length; i < n; i++) {
144
+ notify(this.globals, this.listeners, keys[i], undefined, allData[keys[i]]);
145
+ }
146
+ }
147
+ async cleanup() {
148
+ await this.ready;
149
+ let expired = [], oldValues = new Map();
150
+ await this.driver.map(async (raw, key) => {
151
+ if (key === VERSION_KEY) {
152
+ return;
153
+ }
154
+ let deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
155
+ if (deserialized !== undefined && unwrapped.expired) {
156
+ expired.push(key);
157
+ oldValues.set(key, unwrapped.value);
158
+ }
159
+ });
160
+ if (expired.length > 0) {
161
+ await this.driver.delete(expired);
162
+ for (let i = 0, n = expired.length; i < n; i++) {
163
+ notify(this.globals, this.listeners, expired[i], undefined, oldValues.get(expired[i]));
164
+ }
165
+ }
54
166
  }
55
167
  async count() {
56
- return this.driver.count();
168
+ await this.ready;
169
+ let total = await this.driver.count(), raw = await this.driver.get(VERSION_KEY);
170
+ return raw !== undefined ? total - 1 : total;
57
171
  }
58
172
  async delete(...keys) {
59
- return this.driver.delete(keys);
173
+ await this.ready;
174
+ let oldValues = new Map();
175
+ for (let i = 0, n = keys.length; i < n; i++) {
176
+ let raw = await this.driver.get(keys[i]), deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
177
+ oldValues.set(keys[i], deserialized === undefined ? undefined : unwrapped.value);
178
+ }
179
+ await this.driver.delete(keys);
180
+ for (let i = 0, n = keys.length; i < n; i++) {
181
+ notify(this.globals, this.listeners, keys[i], undefined, oldValues.get(keys[i]));
182
+ }
60
183
  }
61
184
  async filter(fn) {
62
- let i = 0, result = {}, stop = () => { stopped = true; }, stopped = false;
185
+ await this.ready;
186
+ let expired = [], i = 0, result = {}, stop = () => { stopped = true; }, stopped = false;
63
187
  await this.driver.map(async (raw, key) => {
64
- if (stopped) {
188
+ if (stopped || key === VERSION_KEY) {
65
189
  return;
66
190
  }
67
- let value = await deserialize(raw, this.secret);
68
- if (value === undefined) {
191
+ let deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
192
+ if (deserialized === undefined) {
69
193
  return;
70
194
  }
71
- if (await fn({ i: i++, key, stop, value })) {
72
- result[key] = value;
195
+ if (unwrapped.expired) {
196
+ expired.push(key);
197
+ return;
198
+ }
199
+ if (await fn({ i: i++, key, stop, value: unwrapped.value })) {
200
+ result[key] = unwrapped.value;
73
201
  }
74
202
  });
203
+ if (expired.length > 0) {
204
+ this.driver.delete(expired);
205
+ }
75
206
  return result;
76
207
  }
77
- async get(key) {
78
- return deserialize(await this.driver.get(key), this.secret);
208
+ async get(key, factory) {
209
+ await this.ready;
210
+ let deserialized = await deserialize(await this.driver.get(key), this.secret), missing = false, unwrapped = unwrap(deserialized);
211
+ if (deserialized === undefined) {
212
+ missing = true;
213
+ }
214
+ else if (unwrapped.expired) {
215
+ this.driver.delete([key]);
216
+ missing = true;
217
+ }
218
+ if (missing) {
219
+ if (factory) {
220
+ let value = await factory();
221
+ this.set(key, value);
222
+ return value;
223
+ }
224
+ return undefined;
225
+ }
226
+ return unwrapped.value;
79
227
  }
80
228
  async keys() {
81
- return this.driver.keys();
229
+ await this.ready;
230
+ let all = await this.driver.keys();
231
+ return all.filter((k) => k !== VERSION_KEY);
82
232
  }
83
- length() {
84
- return this.driver.count();
233
+ async length() {
234
+ await this.ready;
235
+ return this.count();
85
236
  }
86
- map(fn) {
87
- return this.driver.map(async (raw, key, i) => {
88
- let value = await deserialize(raw, this.secret);
89
- if (value !== undefined) {
90
- await fn(value, key, i);
237
+ async map(fn) {
238
+ await this.ready;
239
+ let expired = [], j = 0;
240
+ await this.driver.map(async (raw, key) => {
241
+ if (key === VERSION_KEY) {
242
+ return;
243
+ }
244
+ let deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
245
+ if (deserialized === undefined) {
246
+ return;
247
+ }
248
+ if (unwrapped.expired) {
249
+ expired.push(key);
250
+ return;
91
251
  }
252
+ await fn(unwrapped.value, key, j++);
92
253
  });
254
+ if (expired.length > 0) {
255
+ this.driver.delete(expired);
256
+ }
93
257
  }
94
258
  async only(...keys) {
95
- let raw = await this.driver.only(keys), result = {};
259
+ await this.ready;
260
+ let expired = [], raw = await this.driver.only(keys), result = {};
96
261
  for (let [key, value] of raw) {
97
- let deserialized = await deserialize(value, this.secret);
98
- if (deserialized !== undefined) {
99
- result[key] = deserialized;
262
+ let deserialized = await deserialize(value, this.secret), unwrapped = unwrap(deserialized);
263
+ if (deserialized === undefined) {
264
+ continue;
100
265
  }
266
+ if (unwrapped.expired) {
267
+ expired.push(key);
268
+ continue;
269
+ }
270
+ result[key] = unwrapped.value;
271
+ }
272
+ if (expired.length > 0) {
273
+ this.driver.delete(expired);
101
274
  }
102
275
  return result;
103
276
  }
277
+ async persist(key) {
278
+ await this.ready;
279
+ let raw = await this.driver.get(key), deserialized = await deserialize(raw, this.secret);
280
+ if (deserialized === undefined) {
281
+ return false;
282
+ }
283
+ let unwrapped = unwrap(deserialized);
284
+ if (unwrapped.expired) {
285
+ this.driver.delete([key]);
286
+ return false;
287
+ }
288
+ if (!unwrapped.hasTTL) {
289
+ return true;
290
+ }
291
+ return this.driver.set(key, await serialize(unwrapped.value, this.secret));
292
+ }
104
293
  async replace(values) {
105
- let entries = [], failed = [];
294
+ await this.ready;
295
+ let entries = [], failed = [], oldValues = new Map();
106
296
  for (let key in values) {
297
+ let raw = await this.driver.get(key), deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
298
+ oldValues.set(key, deserialized === undefined ? undefined : unwrapped.value);
107
299
  try {
108
300
  entries.push([
109
301
  key,
@@ -117,16 +309,64 @@ class Local {
117
309
  if (entries.length > 0) {
118
310
  await this.driver.replace(entries);
119
311
  }
312
+ for (let i = 0, n = entries.length; i < n; i++) {
313
+ let key = entries[i][0];
314
+ notify(this.globals, this.listeners, key, values[key], oldValues.get(key));
315
+ }
120
316
  return failed;
121
317
  }
122
- async set(key, value) {
318
+ async set(key, value, options) {
319
+ await this.ready;
123
320
  try {
124
- return this.driver.set(key, await serialize(value, this.secret));
321
+ let oldRaw = await this.driver.get(key), oldDeserialized = await deserialize(oldRaw, this.secret), oldUnwrapped = unwrap(oldDeserialized), oldValue = oldDeserialized === undefined ? undefined : oldUnwrapped.value, stored;
322
+ if (options?.ttl != null && options.ttl > 0) {
323
+ let envelope = {
324
+ __e: Date.now() + options.ttl,
325
+ __v: value
326
+ };
327
+ stored = await serialize(envelope, this.secret);
328
+ }
329
+ else {
330
+ stored = await serialize(value, this.secret);
331
+ }
332
+ let result = await this.driver.set(key, stored);
333
+ notify(this.globals, this.listeners, key, value, oldValue);
334
+ return result;
125
335
  }
126
336
  catch {
127
337
  return false;
128
338
  }
129
339
  }
340
+ subscribe(keyOrCallback, callback) {
341
+ if (typeof keyOrCallback === 'function') {
342
+ let cb = keyOrCallback;
343
+ this.globals.add(cb);
344
+ return () => { this.globals.delete(cb); };
345
+ }
346
+ let cb = callback, key = keyOrCallback, set = this.listeners.get(key);
347
+ if (!set) {
348
+ set = new Set();
349
+ this.listeners.set(key, set);
350
+ }
351
+ set.add(cb);
352
+ return () => { set.delete(cb); };
353
+ }
354
+ async ttl(key) {
355
+ await this.ready;
356
+ let raw = await this.driver.get(key), deserialized = await deserialize(raw, this.secret);
357
+ if (deserialized === undefined) {
358
+ return -1;
359
+ }
360
+ if (!isEnvelope(deserialized)) {
361
+ return -1;
362
+ }
363
+ let remaining = deserialized.__e - Date.now();
364
+ if (remaining <= 0) {
365
+ this.driver.delete([key]);
366
+ return -1;
367
+ }
368
+ return remaining;
369
+ }
130
370
  }
131
371
  export default (options, secret) => {
132
372
  return new Local(options, secret);
package/build/lz.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare const compress: (input: string) => string;
2
+ declare const decompress: (compressed: string) => string;
3
+ export { compress, decompress };
package/build/lz.js ADDED
@@ -0,0 +1,134 @@
1
+ function emitLiteral(ctx, ch) {
2
+ let code = ch.charCodeAt(0);
3
+ if (code < 256) {
4
+ writeBits(ctx, ctx.numBits, 0);
5
+ writeBits(ctx, 8, code);
6
+ }
7
+ else {
8
+ writeBits(ctx, ctx.numBits, 1);
9
+ writeBits(ctx, 16, code);
10
+ }
11
+ }
12
+ function readBits(ctx, n) {
13
+ let result = 0;
14
+ for (let i = 0; i < n; i++) {
15
+ if (ctx.bitPos > 15) {
16
+ ctx.currentValue = ctx.compressed.charCodeAt(ctx.pos++) - 1;
17
+ ctx.bitPos = 0;
18
+ }
19
+ result = (result << 1) | ((ctx.currentValue >> (15 - ctx.bitPos)) & 1);
20
+ ctx.bitPos++;
21
+ }
22
+ return result;
23
+ }
24
+ function writeBits(ctx, n, value) {
25
+ for (let i = n - 1; i >= 0; i--) {
26
+ ctx.buffer = (ctx.buffer << 1) | ((value >> i) & 1);
27
+ ctx.bitsInBuffer++;
28
+ if (ctx.bitsInBuffer === 16) {
29
+ ctx.output.push(ctx.buffer + 1);
30
+ ctx.buffer = 0;
31
+ ctx.bitsInBuffer = 0;
32
+ }
33
+ }
34
+ }
35
+ const compress = (input) => {
36
+ if (!input) {
37
+ return '';
38
+ }
39
+ let ctx = { bitsInBuffer: 0, buffer: 0, numBits: 2, output: [] }, dictSize = 3, dictionary = new Map(), w = '';
40
+ for (let i = 0, n = input.length; i < n; i++) {
41
+ let c = input[i], wc = w + c;
42
+ if (dictionary.has(wc)) {
43
+ w = wc;
44
+ continue;
45
+ }
46
+ if (w.length > 0) {
47
+ if (dictionary.has(w)) {
48
+ writeBits(ctx, ctx.numBits, dictionary.get(w));
49
+ }
50
+ else {
51
+ emitLiteral(ctx, w);
52
+ }
53
+ dictionary.set(wc, dictSize++);
54
+ if (dictSize > (1 << ctx.numBits)) {
55
+ ctx.numBits++;
56
+ }
57
+ }
58
+ w = c;
59
+ }
60
+ if (w.length > 0) {
61
+ if (dictionary.has(w)) {
62
+ writeBits(ctx, ctx.numBits, dictionary.get(w));
63
+ }
64
+ else {
65
+ emitLiteral(ctx, w);
66
+ }
67
+ }
68
+ dictSize++;
69
+ if (dictSize > (1 << ctx.numBits)) {
70
+ ctx.numBits++;
71
+ }
72
+ writeBits(ctx, ctx.numBits, 2);
73
+ if (ctx.bitsInBuffer > 0) {
74
+ ctx.output.push(((ctx.buffer << (16 - ctx.bitsInBuffer)) & 0xFFFF) + 1);
75
+ }
76
+ ctx.output.push((ctx.bitsInBuffer === 0 ? 16 : ctx.bitsInBuffer) + 1);
77
+ let chars = [];
78
+ for (let i = 0, n = ctx.output.length; i < n; i++) {
79
+ chars.push(String.fromCharCode(ctx.output[i]));
80
+ }
81
+ return chars.join('');
82
+ };
83
+ const decompress = (compressed) => {
84
+ if (!compressed) {
85
+ return '';
86
+ }
87
+ let ctx = { bitPos: 16, compressed: '', currentValue: 0, pos: 0 }, dictSize = 3, dictionary = [], numBits = 2;
88
+ ctx.compressed = compressed.substring(0, compressed.length - 1);
89
+ let code = readBits(ctx, numBits), entry;
90
+ if (code === 0) {
91
+ entry = String.fromCharCode(readBits(ctx, 8));
92
+ }
93
+ else if (code === 1) {
94
+ entry = String.fromCharCode(readBits(ctx, 16));
95
+ }
96
+ else {
97
+ return '';
98
+ }
99
+ let result = [entry], w = entry;
100
+ while (true) {
101
+ let slotIdx = dictionary.length;
102
+ dictionary.push('');
103
+ dictSize++;
104
+ if (dictSize > (1 << numBits)) {
105
+ numBits++;
106
+ }
107
+ code = readBits(ctx, numBits);
108
+ if (code === 2) {
109
+ dictionary.pop();
110
+ break;
111
+ }
112
+ let slotCode = slotIdx + 3;
113
+ if (code === 0) {
114
+ entry = String.fromCharCode(readBits(ctx, 8));
115
+ }
116
+ else if (code === 1) {
117
+ entry = String.fromCharCode(readBits(ctx, 16));
118
+ }
119
+ else if (code === slotCode) {
120
+ entry = w + w[0];
121
+ }
122
+ else if (code >= 3 && code < slotCode) {
123
+ entry = dictionary[code - 3];
124
+ }
125
+ else {
126
+ throw new Error('LZ: invalid decompression code');
127
+ }
128
+ dictionary[slotIdx] = w + entry[0];
129
+ result.push(entry);
130
+ w = entry;
131
+ }
132
+ return result.join('');
133
+ };
134
+ export { compress, decompress };
package/build/types.d.ts CHANGED
@@ -17,9 +17,23 @@ type Filter<T> = (data: {
17
17
  stop: VoidFunction;
18
18
  value: T[keyof T];
19
19
  }) => boolean | Promise<boolean>;
20
+ type MigrationContext = {
21
+ all(): Promise<Record<string, unknown>>;
22
+ };
23
+ type MigrationFn = (old: MigrationContext) => Promise<Record<string, unknown>>;
20
24
  type Options = {
21
- driver?: DriverType.IndexedDB | DriverType.LocalStorage;
25
+ driver?: DriverType.IndexedDB | DriverType.LocalStorage | DriverType.Memory | DriverType.SessionStorage;
26
+ migrations?: Record<number, MigrationFn>;
22
27
  name: string;
23
28
  version: number;
24
29
  };
25
- export type { Driver, Filter, Options };
30
+ type SetOptions = {
31
+ ttl?: number;
32
+ };
33
+ type TTLEnvelope<V> = {
34
+ __e: number;
35
+ __v: V;
36
+ };
37
+ type GlobalCallback<T> = (key: keyof T, newValue: T[keyof T] | undefined, oldValue: T[keyof T] | undefined) => void;
38
+ type KeyCallback<T, K extends keyof T = keyof T> = (newValue: T[K] | undefined, oldValue: T[K] | undefined) => void;
39
+ export type { Driver, Filter, GlobalCallback, KeyCallback, MigrationContext, MigrationFn, Options, SetOptions, TTLEnvelope };
package/package.json CHANGED
@@ -5,7 +5,10 @@
5
5
  },
6
6
  "description": "Web storage utility",
7
7
  "devDependencies": {
8
- "@esportsplus/typescript": "^0.9.2"
8
+ "@esportsplus/typescript": "^0.9.2",
9
+ "fake-indexeddb": "^6.2.5",
10
+ "happy-dom": "^20.8.8",
11
+ "vitest": "^4.1.1"
9
12
  },
10
13
  "main": "build/index.js",
11
14
  "name": "@esportsplus/web-storage",
@@ -16,9 +19,10 @@
16
19
  },
17
20
  "type": "module",
18
21
  "types": "build/index.d.ts",
19
- "version": "0.3.5",
22
+ "version": "0.5.0",
20
23
  "scripts": {
21
24
  "build": "tsc && tsc-alias",
25
+ "test": "vitest run",
22
26
  "-": "-"
23
27
  }
24
28
  }
package/src/constants.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  enum DriverType {
2
2
  IndexedDB,
3
- LocalStorage
3
+ LocalStorage,
4
+ Memory,
5
+ SessionStorage
4
6
  }
5
7
 
6
8
  export { DriverType };
@@ -1,3 +1,4 @@
1
+ import { compress, decompress } from '~/lz';
1
2
  import type { Driver } from '~/types';
2
3
 
3
4
 
@@ -35,6 +36,10 @@ class LocalStorageDriver<T> implements Driver<T> {
35
36
  }
36
37
 
37
38
  try {
39
+ if (value.charCodeAt(0) === 1) {
40
+ return JSON.parse(decompress(value.slice(1)));
41
+ }
42
+
38
43
  return JSON.parse(value);
39
44
  }
40
45
  catch {
@@ -42,6 +47,12 @@ class LocalStorageDriver<T> implements Driver<T> {
42
47
  }
43
48
  }
44
49
 
50
+ private serialize(value: T[keyof T]): string {
51
+ let json = JSON.stringify(value);
52
+
53
+ return json.length >= 100 ? '\x01' + compress(json) : json;
54
+ }
55
+
45
56
 
46
57
  async all(): Promise<T> {
47
58
  let keys = this.getKeys(),
@@ -112,13 +123,13 @@ class LocalStorageDriver<T> implements Driver<T> {
112
123
 
113
124
  async replace(entries: [keyof T, T[keyof T]][]): Promise<void> {
114
125
  for (let i = 0, n = entries.length; i < n; i++) {
115
- localStorage.setItem(this.key(entries[i][0]), JSON.stringify(entries[i][1]));
126
+ localStorage.setItem(this.key(entries[i][0]), this.serialize(entries[i][1]));
116
127
  }
117
128
  }
118
129
 
119
130
  async set(key: keyof T, value: T[keyof T]): Promise<boolean> {
120
131
  try {
121
- localStorage.setItem(this.key(key), JSON.stringify(value));
132
+ localStorage.setItem(this.key(key), this.serialize(value));
122
133
  return true;
123
134
  }
124
135
  catch {