@esportsplus/web-storage 0.3.5 → 0.4.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/constants.d.ts +3 -1
- package/build/constants.js +2 -0
- package/build/drivers/memory.d.ts +16 -0
- package/build/drivers/memory.js +64 -0
- package/build/drivers/sessionstorage.d.ts +19 -0
- package/build/drivers/sessionstorage.js +94 -0
- package/build/index.d.ts +12 -2
- package/build/index.js +271 -31
- package/build/types.d.ts +16 -2
- package/package.json +6 -2
- package/src/constants.ts +3 -1
- package/src/drivers/memory.ts +92 -0
- package/src/drivers/sessionstorage.ts +131 -0
- package/src/index.ts +420 -39
- package/src/types.ts +23 -2
- package/storage/feature-research.md +173 -0
- package/tests/drivers/indexeddb.ts +297 -0
- package/tests/drivers/localstorage.ts +290 -0
- package/tests/drivers/memory.ts +257 -0
- package/tests/drivers/sessionstorage.ts +290 -0
- package/tests/index.ts +1462 -0
- package/vitest.config.ts +16 -0
package/build/constants.d.ts
CHANGED
package/build/constants.js
CHANGED
|
@@ -2,5 +2,7 @@ var DriverType;
|
|
|
2
2
|
(function (DriverType) {
|
|
3
3
|
DriverType[DriverType["IndexedDB"] = 0] = "IndexedDB";
|
|
4
4
|
DriverType[DriverType["LocalStorage"] = 1] = "LocalStorage";
|
|
5
|
+
DriverType[DriverType["Memory"] = 2] = "Memory";
|
|
6
|
+
DriverType[DriverType["SessionStorage"] = 3] = "SessionStorage";
|
|
5
7
|
})(DriverType || (DriverType = {}));
|
|
6
8
|
export { DriverType };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Driver } from '../types.js';
|
|
2
|
+
declare class MemoryDriver<T> implements Driver<T> {
|
|
3
|
+
private store;
|
|
4
|
+
constructor(name: string, _version: number);
|
|
5
|
+
all(): Promise<T>;
|
|
6
|
+
clear(): Promise<void>;
|
|
7
|
+
count(): Promise<number>;
|
|
8
|
+
delete(keys: (keyof T)[]): Promise<void>;
|
|
9
|
+
get(key: keyof T): Promise<T[keyof T] | undefined>;
|
|
10
|
+
keys(): Promise<(keyof T)[]>;
|
|
11
|
+
map(fn: (value: T[keyof T], key: keyof T, i: number) => void | Promise<void>): Promise<void>;
|
|
12
|
+
only(keys: (keyof T)[]): Promise<Map<keyof T, T[keyof T]>>;
|
|
13
|
+
replace(entries: [keyof T, T[keyof T]][]): Promise<void>;
|
|
14
|
+
set(key: keyof T, value: T[keyof T]): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
export { MemoryDriver };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
let stores = new Map();
|
|
2
|
+
class MemoryDriver {
|
|
3
|
+
store;
|
|
4
|
+
constructor(name, _version) {
|
|
5
|
+
let existing = stores.get(name);
|
|
6
|
+
if (existing) {
|
|
7
|
+
this.store = existing;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
this.store = new Map();
|
|
11
|
+
stores.set(name, this.store);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async all() {
|
|
15
|
+
let result = {};
|
|
16
|
+
for (let [key, value] of this.store) {
|
|
17
|
+
result[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
async clear() {
|
|
22
|
+
this.store.clear();
|
|
23
|
+
}
|
|
24
|
+
async count() {
|
|
25
|
+
return this.store.size;
|
|
26
|
+
}
|
|
27
|
+
async delete(keys) {
|
|
28
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
29
|
+
this.store.delete(keys[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async get(key) {
|
|
33
|
+
return this.store.get(key);
|
|
34
|
+
}
|
|
35
|
+
async keys() {
|
|
36
|
+
return [...this.store.keys()];
|
|
37
|
+
}
|
|
38
|
+
async map(fn) {
|
|
39
|
+
let i = 0;
|
|
40
|
+
for (let [key, value] of this.store) {
|
|
41
|
+
await fn(value, key, i++);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async only(keys) {
|
|
45
|
+
let results = new Map();
|
|
46
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
47
|
+
let value = this.store.get(keys[i]);
|
|
48
|
+
if (value !== undefined) {
|
|
49
|
+
results.set(keys[i], value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
async replace(entries) {
|
|
55
|
+
for (let i = 0, n = entries.length; i < n; i++) {
|
|
56
|
+
this.store.set(entries[i][0], entries[i][1]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async set(key, value) {
|
|
60
|
+
this.store.set(key, value);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export { MemoryDriver };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Driver } from '../types.js';
|
|
2
|
+
declare class SessionStorageDriver<T> implements Driver<T> {
|
|
3
|
+
private prefix;
|
|
4
|
+
constructor(name: string, version: number);
|
|
5
|
+
private getKeys;
|
|
6
|
+
private key;
|
|
7
|
+
private parse;
|
|
8
|
+
all(): Promise<T>;
|
|
9
|
+
clear(): Promise<void>;
|
|
10
|
+
count(): Promise<number>;
|
|
11
|
+
delete(keys: (keyof T)[]): Promise<void>;
|
|
12
|
+
get(key: keyof T): Promise<T[keyof T] | undefined>;
|
|
13
|
+
keys(): Promise<(keyof T)[]>;
|
|
14
|
+
map(fn: (value: T[keyof T], key: keyof T, i: number) => void | Promise<void>): Promise<void>;
|
|
15
|
+
only(keys: (keyof T)[]): Promise<Map<keyof T, T[keyof T]>>;
|
|
16
|
+
replace(entries: [keyof T, T[keyof T]][]): Promise<void>;
|
|
17
|
+
set(key: keyof T, value: T[keyof T]): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
export { SessionStorageDriver };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
class SessionStorageDriver {
|
|
2
|
+
prefix;
|
|
3
|
+
constructor(name, version) {
|
|
4
|
+
this.prefix = `${name}:${version}:`;
|
|
5
|
+
}
|
|
6
|
+
getKeys() {
|
|
7
|
+
let keys = [];
|
|
8
|
+
for (let i = 0, n = sessionStorage.length; i < n; i++) {
|
|
9
|
+
let key = sessionStorage.key(i);
|
|
10
|
+
if (key && key.startsWith(this.prefix)) {
|
|
11
|
+
keys.push(key.slice(this.prefix.length));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return keys;
|
|
15
|
+
}
|
|
16
|
+
key(key) {
|
|
17
|
+
return this.prefix + String(key);
|
|
18
|
+
}
|
|
19
|
+
parse(value) {
|
|
20
|
+
if (value === null) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async all() {
|
|
31
|
+
let keys = this.getKeys(), result = {};
|
|
32
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
33
|
+
let value = this.parse(sessionStorage.getItem(this.prefix + keys[i]));
|
|
34
|
+
if (value !== undefined) {
|
|
35
|
+
result[keys[i]] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
async clear() {
|
|
41
|
+
let keys = this.getKeys();
|
|
42
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
43
|
+
sessionStorage.removeItem(this.prefix + keys[i]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async count() {
|
|
47
|
+
return this.getKeys().length;
|
|
48
|
+
}
|
|
49
|
+
async delete(keys) {
|
|
50
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
51
|
+
sessionStorage.removeItem(this.key(keys[i]));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async get(key) {
|
|
55
|
+
return this.parse(sessionStorage.getItem(this.key(key)));
|
|
56
|
+
}
|
|
57
|
+
async keys() {
|
|
58
|
+
return this.getKeys();
|
|
59
|
+
}
|
|
60
|
+
async map(fn) {
|
|
61
|
+
let keys = this.getKeys();
|
|
62
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
63
|
+
let value = this.parse(sessionStorage.getItem(this.prefix + keys[i]));
|
|
64
|
+
if (value !== undefined) {
|
|
65
|
+
await fn(value, keys[i], i);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async only(keys) {
|
|
70
|
+
let results = new Map();
|
|
71
|
+
for (let i = 0, n = keys.length; i < n; i++) {
|
|
72
|
+
let value = this.parse(sessionStorage.getItem(this.key(keys[i])));
|
|
73
|
+
if (value !== undefined) {
|
|
74
|
+
results.set(keys[i], value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
async replace(entries) {
|
|
80
|
+
for (let i = 0, n = entries.length; i < n; i++) {
|
|
81
|
+
sessionStorage.setItem(this.key(entries[i][0]), JSON.stringify(entries[i][1]));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async set(key, value) {
|
|
85
|
+
try {
|
|
86
|
+
sessionStorage.setItem(this.key(key), JSON.stringify(value));
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export { SessionStorageDriver };
|
package/build/index.d.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
import type { Filter, Options } from './types.js';
|
|
1
|
+
import type { Filter, GlobalCallback, KeyCallback, Options, SetOptions } from './types.js';
|
|
2
2
|
declare class Local<T> {
|
|
3
3
|
private driver;
|
|
4
|
+
private globals;
|
|
5
|
+
private listeners;
|
|
6
|
+
private ready;
|
|
4
7
|
private secret;
|
|
8
|
+
private version;
|
|
5
9
|
constructor(options: Options, secret?: string);
|
|
6
10
|
all(): Promise<T>;
|
|
7
11
|
clear(): Promise<void>;
|
|
12
|
+
cleanup(): Promise<void>;
|
|
8
13
|
count(): Promise<number>;
|
|
9
14
|
delete(...keys: (keyof T)[]): Promise<void>;
|
|
10
15
|
filter(fn: Filter<T>): Promise<T>;
|
|
11
16
|
get(key: keyof T): Promise<T[keyof T] | undefined>;
|
|
17
|
+
get(key: keyof T, factory: () => T[keyof T] | Promise<T[keyof T]>): Promise<T[keyof T]>;
|
|
12
18
|
keys(): Promise<(keyof T)[]>;
|
|
13
19
|
length(): Promise<number>;
|
|
14
20
|
map(fn: (value: T[keyof T], key: keyof T, i: number) => void | Promise<void>): Promise<void>;
|
|
15
21
|
only(...keys: (keyof T)[]): Promise<T>;
|
|
22
|
+
persist(key: keyof T): Promise<boolean>;
|
|
16
23
|
replace(values: Partial<T>): Promise<string[]>;
|
|
17
|
-
set(key: keyof T, value: T[keyof T]): Promise<boolean>;
|
|
24
|
+
set(key: keyof T, value: T[keyof T], options?: SetOptions): Promise<boolean>;
|
|
25
|
+
subscribe(callback: GlobalCallback<T>): () => void;
|
|
26
|
+
subscribe<K extends keyof T>(key: K, callback: KeyCallback<T, K>): () => void;
|
|
27
|
+
ttl(key: keyof T): Promise<number>;
|
|
18
28
|
}
|
|
19
29
|
declare const _default: <T>(options: Options, secret?: string) => Local<T>;
|
|
20
30
|
export default _default;
|
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
|
-
|
|
117
|
+
await this.ready;
|
|
118
|
+
let expired = [], raw = await this.driver.all(), result = {};
|
|
44
119
|
for (let key in raw) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
if (
|
|
191
|
+
let deserialized = await deserialize(raw, this.secret), unwrapped = unwrap(deserialized);
|
|
192
|
+
if (deserialized === undefined) {
|
|
69
193
|
return;
|
|
70
194
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
await this.ready;
|
|
230
|
+
let all = await this.driver.keys();
|
|
231
|
+
return all.filter((k) => k !== VERSION_KEY);
|
|
82
232
|
}
|
|
83
|
-
length() {
|
|
84
|
-
|
|
233
|
+
async length() {
|
|
234
|
+
await this.ready;
|
|
235
|
+
return this.count();
|
|
85
236
|
}
|
|
86
|
-
map(fn) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/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
|
-
|
|
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.
|
|
22
|
+
"version": "0.4.0",
|
|
20
23
|
"scripts": {
|
|
21
24
|
"build": "tsc && tsc-alias",
|
|
25
|
+
"test": "vitest run",
|
|
22
26
|
"-": "-"
|
|
23
27
|
}
|
|
24
28
|
}
|