jg-storage 0.0.0-stage → 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/indexed-db-store/index.d.ts +1 -0
- package/dist/indexed-db-store/index.js +3 -0
- package/dist/indexed-db-store/index.js.map +1 -0
- package/dist/indexed-db-store/indexed-db-store.d.ts +22 -0
- package/dist/indexed-db-store/indexed-db-store.js +95 -0
- package/dist/indexed-db-store/indexed-db-store.js.map +1 -0
- package/dist/memory-store/index.d.ts +1 -0
- package/dist/memory-store/index.js +3 -0
- package/dist/memory-store/index.js.map +1 -0
- package/dist/memory-store/memory-store.d.ts +16 -0
- package/dist/memory-store/memory-store.js +34 -0
- package/dist/memory-store/memory-store.js.map +1 -0
- package/dist/shared/constants.d.ts +4 -0
- package/dist/shared/constants.js +7 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/errors.d.ts +5 -0
- package/dist/shared/errors.js +7 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/types.d.ts +22 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/utilities.d.ts +39 -0
- package/dist/shared/utilities.js +60 -0
- package/dist/shared/utilities.js.map +1 -0
- package/dist/web-storage-store/index.d.ts +1 -0
- package/dist/web-storage-store/index.js +3 -0
- package/dist/web-storage-store/index.js.map +1 -0
- package/dist/web-storage-store/web-storage-store.d.ts +43 -0
- package/dist/web-storage-store/web-storage-store.js +132 -0
- package/dist/web-storage-store/web-storage-store.js.map +1 -0
- package/package.json +64 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
The first release of `jg-storage`, which replaces `browser-keyval-stores` 1.1.1. Records written by `browser-keyval-stores` read back unchanged.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `createLocalStore`, `createSessionStore`, `createMemoryStore`, and `createIndexedDbStore`, which replace the store classes.
|
|
10
|
+
- The `fallback: 'memory'` and `parse` options.
|
|
11
|
+
- `ERRORS`, the codes of every error the package throws, with the new `STORE_UNAVAILABLE` code.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Web storage stores accept any value JSON can represent, primitives included.
|
|
16
|
+
- A store whose mechanism is unavailable keeps records in memory only with `fallback: 'memory'`. Without it, `set` and `del` throw `STORE_UNAVAILABLE`.
|
|
17
|
+
- Web storage is read from `globalThis` on a store's first call, never from `window`.
|
|
18
|
+
- `IRecordID` is renamed to `IRecordId`, and the store interfaces to `ISyncStore` and `IAsyncStore`.
|
|
19
|
+
- Errors are `Exception`s from `jg-exception`.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- The `LocalStorageStore`, `SessionStorageStore`, and `IndexedDBStore` classes.
|
|
24
|
+
- The `INVALID_JSON_DATA` code.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jesus Graterol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# jg-storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed key-value stores over localStorage, sessionStorage, IndexedDB, and memory, for browsers and workers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install jg-storage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Keep settings in localStorage, with a root record and records by ID:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createLocalStore } from 'jg-storage';
|
|
17
|
+
|
|
18
|
+
const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');
|
|
19
|
+
settings.set(undefined, { theme: 'dark' }); // stored under the key 'settings'
|
|
20
|
+
settings.get(); // { theme: 'dark' }
|
|
21
|
+
settings.set('draft', { theme: 'light' }); // stored under the key 'settings_draft'
|
|
22
|
+
settings.del('draft');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Keep records in IndexedDB, from a page or a service worker:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createIndexedDbStore } from 'jg-storage';
|
|
29
|
+
|
|
30
|
+
const devices = createIndexedDbStore<{ token: string }>('devices');
|
|
31
|
+
await devices.set('phone', { token: 'abc' });
|
|
32
|
+
await devices.get('phone'); // { token: 'abc' }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Validate what you read, and fall back to memory where storage is blocked:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createSessionStore, ERRORS } from 'jg-storage';
|
|
39
|
+
import { hasErrorCode } from 'jg-exception';
|
|
40
|
+
import { z } from 'zod';
|
|
41
|
+
|
|
42
|
+
const Step = z.number().int().min(1);
|
|
43
|
+
const wizard = createSessionStore('wizard-step', {
|
|
44
|
+
fallback: 'memory', // without it, set and del throw STORE_UNAVAILABLE
|
|
45
|
+
parse: (value) => Step.parse(value), // runs on every stored value get reads
|
|
46
|
+
});
|
|
47
|
+
wizard.set(undefined, 2);
|
|
48
|
+
wizard.get(); // 2
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
createSessionStore('other').set(undefined, 1);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (hasErrorCode(error, ERRORS.STORE_UNAVAILABLE)) {
|
|
54
|
+
// sessionStorage is unavailable in this context
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every store probes its mechanism on its first call, never when it's created, so creating stores at module scope is safe during server-side rendering.
|
|
60
|
+
|
|
61
|
+
## Coming from `browser-keyval-stores`
|
|
62
|
+
|
|
63
|
+
| `browser-keyval-stores` | `jg-storage` |
|
|
64
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
65
|
+
| `new LocalStorageStore<T>(id)` | `createLocalStore<T>(id, options?)` |
|
|
66
|
+
| `new SessionStorageStore<T>(id)` | `createSessionStore<T>(id, options?)` |
|
|
67
|
+
| `new IndexedDBStore<T>(id)` | `createIndexedDbStore<T>(id, options?)` |
|
|
68
|
+
| the internal temp-memory store | `createMemoryStore<T>(id, options?)` |
|
|
69
|
+
| `ILocalStorageStore<T>`, `ISessionStorageStore<T>` | `ISyncStore<T>` |
|
|
70
|
+
| `IIndexedDBStore<T>` | `IAsyncStore<T>` |
|
|
71
|
+
| `IRecordID` | `IRecordId` |
|
|
72
|
+
| the silent memory fallback | `{ fallback: 'memory' }`; without it, `set` and `del` throw `STORE_UNAVAILABLE` |
|
|
73
|
+
| the codes, not exported | `ERRORS` |
|
|
74
|
+
|
|
75
|
+
`IStoreMechanism` and the `get`, `set`, and `del` methods keep their names and argument order.
|
|
76
|
+
|
|
77
|
+
- Stored data is compatible: the keys, the compatibility probe's record, idb-keyval's default `keyval-store` database, JSON values in web storage, and structured-clone values in IndexedDB are all the same, so records written by `browser-keyval-stores` read back unchanged.
|
|
78
|
+
- `set` on a web storage store accepts any value JSON can represent, primitives included. It used to accept only objects and arrays and throw `INVALID_JSON_DATA`, which is gone. A value JSON can't represent, such as `undefined` or a `bigint`, throws `FAILED_TO_STRINGIFY_JSON`.
|
|
79
|
+
- When a mechanism is unavailable, a store without `fallback: 'memory'` returns `undefined` from `get` and throws `STORE_UNAVAILABLE` from `set` and `del`, instead of silently keeping records in memory.
|
|
80
|
+
- The new `parse` option runs on every stored value `get` reads, and its errors propagate.
|
|
81
|
+
- Nothing reads `window`: web storage is read from `globalThis` on the first call, and IndexedDB keeps using `globalThis.indexedDB`, so the IndexedDB store also works in workers.
|
|
82
|
+
- Errors are `Exception`s from `jg-exception`.
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
The full API reference is at https://jg-packages.jesusgraterol.dev/packages/jg-storage/.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[MIT](https://github.com/jesusgraterol/jg-packages/blob/main/packages/jg-storage/LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { IAsyncStore, IRecordId, IStoreMechanism, IStoreOptions, ISyncStore, } from './shared/index.js';
|
|
2
|
+
export { ERRORS } from './shared/index.js';
|
|
3
|
+
export { createMemoryStore } from './memory-store/index.js';
|
|
4
|
+
export { createLocalStore, createSessionStore } from './web-storage-store/index.js';
|
|
5
|
+
export { createIndexedDbStore } from './indexed-db-store/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// error codes
|
|
2
|
+
export { ERRORS } from './shared/index.js';
|
|
3
|
+
// memory stores
|
|
4
|
+
export { createMemoryStore } from './memory-store/index.js';
|
|
5
|
+
// web storage stores
|
|
6
|
+
export { createLocalStore, createSessionStore } from './web-storage-store/index.js';
|
|
7
|
+
// IndexedDB stores
|
|
8
|
+
export { createIndexedDbStore } from './indexed-db-store/index.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,cAAc;AACd,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,qBAAqB;AACrB,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEpF,mBAAmB;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["// types\nexport type {\n IAsyncStore,\n IRecordId,\n IStoreMechanism,\n IStoreOptions,\n ISyncStore,\n} from './shared/index.js';\n\n// error codes\nexport { ERRORS } from './shared/index.js';\n\n// memory stores\nexport { createMemoryStore } from './memory-store/index.js';\n\n// web storage stores\nexport { createLocalStore, createSessionStore } from './web-storage-store/index.js';\n\n// IndexedDB stores\nexport { createIndexedDbStore } from './indexed-db-store/index.js';\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createIndexedDbStore } from './indexed-db-store.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/indexed-db-store/index.ts"],"names":[],"mappings":"AAAA,mBAAmB;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["// IndexedDB stores\nexport { createIndexedDbStore } from './indexed-db-store.js';\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type IAsyncStore, type IStoreOptions } from '../shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a store over IndexedDB, which works in pages and workers alike. Records live in
|
|
4
|
+
* idb-keyval's default database, `keyval-store`, under the same keys browser-keyval-stores used,
|
|
5
|
+
* so their records read back unchanged. Values are stored with the structured clone algorithm,
|
|
6
|
+
* so dates, maps, and binary data keep their types. IndexedDB is read and probed on the first
|
|
7
|
+
* call, never when the store is created.
|
|
8
|
+
* @param id The identifier every record key starts with.
|
|
9
|
+
* @param options fallback: 'memory' keeps records in memory when IndexedDB is unavailable, and
|
|
10
|
+
* parse runs on every stored value get reads.
|
|
11
|
+
* @returns The store.
|
|
12
|
+
* @example
|
|
13
|
+
* const devices = createIndexedDbStore<{ token: string }>('devices');
|
|
14
|
+
* await devices.set('phone', { token: 'abc' }); // stored under the key 'devices_phone'
|
|
15
|
+
* await devices.get('phone'); // { token: 'abc' }
|
|
16
|
+
* await devices.del('phone');
|
|
17
|
+
* @throws
|
|
18
|
+
* - STORE_UNAVAILABLE: set or del ran while IndexedDB is unavailable, without a fallback.
|
|
19
|
+
*
|
|
20
|
+
* Errors from IndexedDB itself, such as a value it can't clone, propagate.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createIndexedDbStore: <T>(id: string, options?: IStoreOptions<T>) => IAsyncStore<T>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { del, get, set } from 'idb-keyval';
|
|
2
|
+
import { createMemoryStore } from '../memory-store/index.js';
|
|
3
|
+
import { STORAGE_TEST_DATA, buildDataKey, buildUnavailableException, parseValue, } from '../shared/index.js';
|
|
4
|
+
// IndexedDB, read from globalThis so the store also works in workers. Some environments lack it,
|
|
5
|
+
// so it's optional here
|
|
6
|
+
const indexedDbGlobals = globalThis;
|
|
7
|
+
/**
|
|
8
|
+
* Probes whether IndexedDB accepts writes, by writing and deleting a record in idb-keyval's
|
|
9
|
+
* default database.
|
|
10
|
+
* @returns Whether IndexedDB is available.
|
|
11
|
+
*/
|
|
12
|
+
const probeIndexedDb = async () => {
|
|
13
|
+
try {
|
|
14
|
+
if (!indexedDbGlobals.indexedDB) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
await set(STORAGE_TEST_DATA.key, STORAGE_TEST_DATA.value);
|
|
18
|
+
await del(STORAGE_TEST_DATA.key);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Creates a store over IndexedDB, which works in pages and workers alike. Records live in
|
|
27
|
+
* idb-keyval's default database, `keyval-store`, under the same keys browser-keyval-stores used,
|
|
28
|
+
* so their records read back unchanged. Values are stored with the structured clone algorithm,
|
|
29
|
+
* so dates, maps, and binary data keep their types. IndexedDB is read and probed on the first
|
|
30
|
+
* call, never when the store is created.
|
|
31
|
+
* @param id The identifier every record key starts with.
|
|
32
|
+
* @param options fallback: 'memory' keeps records in memory when IndexedDB is unavailable, and
|
|
33
|
+
* parse runs on every stored value get reads.
|
|
34
|
+
* @returns The store.
|
|
35
|
+
* @example
|
|
36
|
+
* const devices = createIndexedDbStore<{ token: string }>('devices');
|
|
37
|
+
* await devices.set('phone', { token: 'abc' }); // stored under the key 'devices_phone'
|
|
38
|
+
* await devices.get('phone'); // { token: 'abc' }
|
|
39
|
+
* await devices.del('phone');
|
|
40
|
+
* @throws
|
|
41
|
+
* - STORE_UNAVAILABLE: set or del ran while IndexedDB is unavailable, without a fallback.
|
|
42
|
+
*
|
|
43
|
+
* Errors from IndexedDB itself, such as a value it can't clone, propagate.
|
|
44
|
+
*/
|
|
45
|
+
export const createIndexedDbStore = (id, options = {}) => {
|
|
46
|
+
let compatibility;
|
|
47
|
+
let memory;
|
|
48
|
+
// the probe runs once, even when several calls start before it settles
|
|
49
|
+
const isCompatible = () => {
|
|
50
|
+
compatibility ??= probeIndexedDb();
|
|
51
|
+
return compatibility;
|
|
52
|
+
};
|
|
53
|
+
// the memory store an unavailable IndexedDB falls back to, when the options ask for one
|
|
54
|
+
const getMemory = () => {
|
|
55
|
+
if (options.fallback === 'memory') {
|
|
56
|
+
memory ??= createMemoryStore(id, options);
|
|
57
|
+
}
|
|
58
|
+
return memory;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
mechanism: 'indexedDB',
|
|
63
|
+
isCompatible,
|
|
64
|
+
get: async (recordId) => {
|
|
65
|
+
if (!(await isCompatible())) {
|
|
66
|
+
return getMemory()?.get(recordId);
|
|
67
|
+
}
|
|
68
|
+
const value = await get(buildDataKey(id, recordId));
|
|
69
|
+
return value === undefined ? undefined : parseValue(value, options);
|
|
70
|
+
},
|
|
71
|
+
set: async (recordId, value) => {
|
|
72
|
+
if (await isCompatible()) {
|
|
73
|
+
await set(buildDataKey(id, recordId), value);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const fallback = getMemory();
|
|
77
|
+
if (fallback === undefined) {
|
|
78
|
+
throw buildUnavailableException(id, 'indexedDB');
|
|
79
|
+
}
|
|
80
|
+
fallback.set(recordId, value);
|
|
81
|
+
},
|
|
82
|
+
del: async (recordId) => {
|
|
83
|
+
if (await isCompatible()) {
|
|
84
|
+
await del(buildDataKey(id, recordId));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const fallback = getMemory();
|
|
88
|
+
if (fallback === undefined) {
|
|
89
|
+
throw buildUnavailableException(id, 'indexedDB');
|
|
90
|
+
}
|
|
91
|
+
fallback.del(recordId);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=indexed-db-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-db-store.js","sourceRoot":"","sources":["../../src/indexed-db-store/indexed-db-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { del, get, set } from 'idb-keyval';\n\nimport { createMemoryStore } from '../memory-store/index.js';\nimport {\n STORAGE_TEST_DATA,\n buildDataKey,\n buildUnavailableException,\n parseValue,\n type IAsyncStore,\n type IRecordId,\n type IStoreOptions,\n type ISyncStore,\n} from '../shared/index.js';\n\n// IndexedDB, read from globalThis so the store also works in workers. Some environments lack it,\n// so it's optional here\nconst indexedDbGlobals: { indexedDB?: IDBFactory } = globalThis;\n\n/**\n * Probes whether IndexedDB accepts writes, by writing and deleting a record in idb-keyval's\n * default database.\n * @returns Whether IndexedDB is available.\n */\nconst probeIndexedDb = async (): Promise<boolean> => {\n try {\n if (!indexedDbGlobals.indexedDB) {\n return false;\n }\n await set(STORAGE_TEST_DATA.key, STORAGE_TEST_DATA.value);\n await del(STORAGE_TEST_DATA.key);\n return true;\n } catch {\n return false;\n }\n};\n\n/**\n * Creates a store over IndexedDB, which works in pages and workers alike. Records live in\n * idb-keyval's default database, `keyval-store`, under the same keys browser-keyval-stores used,\n * so their records read back unchanged. Values are stored with the structured clone algorithm,\n * so dates, maps, and binary data keep their types. IndexedDB is read and probed on the first\n * call, never when the store is created.\n * @param id The identifier every record key starts with.\n * @param options fallback: 'memory' keeps records in memory when IndexedDB is unavailable, and\n * parse runs on every stored value get reads.\n * @returns The store.\n * @example\n * const devices = createIndexedDbStore<{ token: string }>('devices');\n * await devices.set('phone', { token: 'abc' }); // stored under the key 'devices_phone'\n * await devices.get('phone'); // { token: 'abc' }\n * await devices.del('phone');\n * @throws\n * - STORE_UNAVAILABLE: set or del ran while IndexedDB is unavailable, without a fallback.\n *\n * Errors from IndexedDB itself, such as a value it can't clone, propagate.\n */\nexport const createIndexedDbStore = <T>(\n id: string,\n options: IStoreOptions<T> = {},\n): IAsyncStore<T> => {\n let compatibility: Promise<boolean> | undefined;\n let memory: ISyncStore<T> | undefined;\n\n // the probe runs once, even when several calls start before it settles\n const isCompatible = (): Promise<boolean> => {\n compatibility ??= probeIndexedDb();\n return compatibility;\n };\n\n // the memory store an unavailable IndexedDB falls back to, when the options ask for one\n const getMemory = (): ISyncStore<T> | undefined => {\n if (options.fallback === 'memory') {\n memory ??= createMemoryStore(id, options);\n }\n return memory;\n };\n\n return {\n id,\n mechanism: 'indexedDB',\n isCompatible,\n get: async (recordId?: IRecordId) => {\n if (!(await isCompatible())) {\n return getMemory()?.get(recordId);\n }\n const value: unknown = await get(buildDataKey(id, recordId));\n return value === undefined ? undefined : parseValue(value, options);\n },\n set: async (recordId: IRecordId, value: T) => {\n if (await isCompatible()) {\n await set(buildDataKey(id, recordId), value);\n return;\n }\n const fallback = getMemory();\n if (fallback === undefined) {\n throw buildUnavailableException(id, 'indexedDB');\n }\n fallback.set(recordId, value);\n },\n del: async (recordId?: IRecordId) => {\n if (await isCompatible()) {\n await del(buildDataKey(id, recordId));\n return;\n }\n const fallback = getMemory();\n if (fallback === undefined) {\n throw buildUnavailableException(id, 'indexedDB');\n }\n fallback.del(recordId);\n },\n };\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMemoryStore } from './memory-store.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/memory-store/index.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["// memory stores\nexport { createMemoryStore } from './memory-store.js';\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type IStoreOptions, type ISyncStore } from '../shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a store that keeps its records in memory for as long as the store exists. Values are
|
|
4
|
+
* kept as they are, without serializing them, and the store is always available.
|
|
5
|
+
* @param id The identifier every record key starts with.
|
|
6
|
+
* @param options parse runs on every stored value get reads. fallback has no effect, because
|
|
7
|
+
* memory is always available.
|
|
8
|
+
* @returns The store.
|
|
9
|
+
* @example
|
|
10
|
+
* const drafts = createMemoryStore<{ text: string }>('drafts');
|
|
11
|
+
* drafts.set('post-1', { text: 'Hello' });
|
|
12
|
+
* drafts.get('post-1'); // { text: 'Hello' }
|
|
13
|
+
* drafts.del('post-1');
|
|
14
|
+
* drafts.get('post-1'); // undefined
|
|
15
|
+
*/
|
|
16
|
+
export declare const createMemoryStore: <T>(id: string, options?: IStoreOptions<T>) => ISyncStore<T>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { buildDataKey, parseValue, } from '../shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a store that keeps its records in memory for as long as the store exists. Values are
|
|
4
|
+
* kept as they are, without serializing them, and the store is always available.
|
|
5
|
+
* @param id The identifier every record key starts with.
|
|
6
|
+
* @param options parse runs on every stored value get reads. fallback has no effect, because
|
|
7
|
+
* memory is always available.
|
|
8
|
+
* @returns The store.
|
|
9
|
+
* @example
|
|
10
|
+
* const drafts = createMemoryStore<{ text: string }>('drafts');
|
|
11
|
+
* drafts.set('post-1', { text: 'Hello' });
|
|
12
|
+
* drafts.get('post-1'); // { text: 'Hello' }
|
|
13
|
+
* drafts.del('post-1');
|
|
14
|
+
* drafts.get('post-1'); // undefined
|
|
15
|
+
*/
|
|
16
|
+
export const createMemoryStore = (id, options = {}) => {
|
|
17
|
+
const records = new Map();
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
mechanism: 'tempMemory',
|
|
21
|
+
isCompatible: () => true,
|
|
22
|
+
get: (recordId) => {
|
|
23
|
+
const value = records.get(buildDataKey(id, recordId));
|
|
24
|
+
return value === undefined ? undefined : parseValue(value, options);
|
|
25
|
+
},
|
|
26
|
+
set: (recordId, value) => {
|
|
27
|
+
records.set(buildDataKey(id, recordId), value);
|
|
28
|
+
},
|
|
29
|
+
del: (recordId) => {
|
|
30
|
+
records.delete(buildDataKey(id, recordId));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=memory-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-store.js","sourceRoot":"","sources":["../../src/memory-store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,UAAU,GAIX,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAI,EAAU,EAAE,UAA4B,EAAE,EAAiB,EAAE;IAChG,MAAM,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;IACrC,OAAO;QACL,EAAE;QACF,SAAS,EAAE,YAAY;QACvB,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI;QACxB,GAAG,EAAE,CAAC,QAAoB,EAAE,EAAE;YAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;YACtD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACtE,CAAC;QACD,GAAG,EAAE,CAAC,QAAmB,EAAE,KAAQ,EAAE,EAAE;YACrC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;QACjD,CAAC;QACD,GAAG,EAAE,CAAC,QAAoB,EAAE,EAAE;YAC5B,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import {\n buildDataKey,\n parseValue,\n type IRecordId,\n type IStoreOptions,\n type ISyncStore,\n} from '../shared/index.js';\n\n/**\n * Creates a store that keeps its records in memory for as long as the store exists. Values are\n * kept as they are, without serializing them, and the store is always available.\n * @param id The identifier every record key starts with.\n * @param options parse runs on every stored value get reads. fallback has no effect, because\n * memory is always available.\n * @returns The store.\n * @example\n * const drafts = createMemoryStore<{ text: string }>('drafts');\n * drafts.set('post-1', { text: 'Hello' });\n * drafts.get('post-1'); // { text: 'Hello' }\n * drafts.del('post-1');\n * drafts.get('post-1'); // undefined\n */\nexport const createMemoryStore = <T>(id: string, options: IStoreOptions<T> = {}): ISyncStore<T> => {\n const records = new Map<string, T>();\n return {\n id,\n mechanism: 'tempMemory',\n isCompatible: () => true,\n get: (recordId?: IRecordId) => {\n const value = records.get(buildDataKey(id, recordId));\n return value === undefined ? undefined : parseValue(value, options);\n },\n set: (recordId: IRecordId, value: T) => {\n records.set(buildDataKey(id, recordId), value);\n },\n del: (recordId?: IRecordId) => {\n records.delete(buildDataKey(id, recordId));\n },\n };\n};\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// the record written and removed to probe whether a mechanism accepts writes. Its key and value
|
|
2
|
+
// match browser-keyval-stores', so both packages probe the same record
|
|
3
|
+
export const STORAGE_TEST_DATA = {
|
|
4
|
+
key: 'SOME_KEY_2d22df80-fc4b-498a-a4a7-734daa71c8dd',
|
|
5
|
+
value: 'Hello world!',
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,GAAG,EAAE,+CAA+C;IACpD,KAAK,EAAE,cAAc;CACb,CAAC","sourcesContent":["// the record written and removed to probe whether a mechanism accepts writes. Its key and value\n// match browser-keyval-stores', so both packages probe the same record\nexport const STORAGE_TEST_DATA = {\n key: 'SOME_KEY_2d22df80-fc4b-498a-a4a7-734daa71c8dd',\n value: 'Hello world!',\n} as const;\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/shared/errors.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,wBAAwB,EAAE,0BAA0B;IACpD,oBAAoB,EAAE,sBAAsB;IAC5C,iBAAiB,EAAE,mBAAmB;CAC9B,CAAC","sourcesContent":["// the codes for every error thrown by the package\nexport const ERRORS = {\n FAILED_TO_STRINGIFY_JSON: 'FAILED_TO_STRINGIFY_JSON',\n FAILED_TO_PARSE_JSON: 'FAILED_TO_PARSE_JSON',\n STORE_UNAVAILABLE: 'STORE_UNAVAILABLE',\n} as const;\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { STORAGE_TEST_DATA } from './constants.js';
|
|
2
|
+
export { ERRORS } from './errors.js';
|
|
3
|
+
export type { IAsyncStore, IRecordId, IStoreMechanism, IStoreOptions, ISyncStore, } from './types.js';
|
|
4
|
+
export { buildDataKey, buildUnavailableException, parseJSON, parseValue, stringifyJSON, } from './utilities.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// constants
|
|
2
|
+
export { STORAGE_TEST_DATA } from './constants.js';
|
|
3
|
+
// errors
|
|
4
|
+
export { ERRORS } from './errors.js';
|
|
5
|
+
// utilities
|
|
6
|
+
export { buildDataKey, buildUnavailableException, parseJSON, parseValue, stringifyJSON, } from './utilities.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,YAAY;AACZ,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEnD,SAAS;AACT,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAWrC,YAAY;AACZ,OAAO,EACL,YAAY,EACZ,yBAAyB,EACzB,SAAS,EACT,UAAU,EACV,aAAa,GACd,MAAM,gBAAgB,CAAC","sourcesContent":["// constants\nexport { STORAGE_TEST_DATA } from './constants.js';\n\n// errors\nexport { ERRORS } from './errors.js';\n\n// types\nexport type {\n IAsyncStore,\n IRecordId,\n IStoreMechanism,\n IStoreOptions,\n ISyncStore,\n} from './types.js';\n\n// utilities\nexport {\n buildDataKey,\n buildUnavailableException,\n parseJSON,\n parseValue,\n stringifyJSON,\n} from './utilities.js';\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type IStoreMechanism = 'tempMemory' | 'localStorage' | 'sessionStorage' | 'indexedDB';
|
|
2
|
+
export type IRecordId = undefined | string | number;
|
|
3
|
+
export type IStoreOptions<T> = {
|
|
4
|
+
fallback?: 'memory';
|
|
5
|
+
parse?: (value: unknown) => T;
|
|
6
|
+
};
|
|
7
|
+
export type ISyncStore<T> = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly mechanism: IStoreMechanism;
|
|
10
|
+
isCompatible(): boolean;
|
|
11
|
+
get(recordId?: IRecordId): T | undefined;
|
|
12
|
+
set(recordId: IRecordId, value: T): void;
|
|
13
|
+
del(recordId?: IRecordId): void;
|
|
14
|
+
};
|
|
15
|
+
export type IAsyncStore<T> = {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly mechanism: IStoreMechanism;
|
|
18
|
+
isCompatible(): Promise<boolean>;
|
|
19
|
+
get(recordId?: IRecordId): Promise<T | undefined>;
|
|
20
|
+
set(recordId: IRecordId, value: T): Promise<void>;
|
|
21
|
+
del(recordId?: IRecordId): Promise<void>;
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"","sourcesContent":["// the mechanism a store keeps its records in\nexport type IStoreMechanism = 'tempMemory' | 'localStorage' | 'sessionStorage' | 'indexedDB';\n\n// identifies a record in a store. undefined addresses the store's root record, and a string or a\n// number addresses the record stored under `${storeId}_${recordId}`\nexport type IRecordId = undefined | string | number;\n\n// the options every store factory accepts\nexport type IStoreOptions<T> = {\n // keeps records in a memory map owned by the store when its mechanism is unavailable. Without it,\n // get returns undefined and set and del throw STORE_UNAVAILABLE\n fallback?: 'memory';\n\n // validates or converts every stored value get reads, such as a schema's parse. Its errors\n // propagate\n parse?: (value: unknown) => T;\n};\n\n// a store whose methods return their results directly: localStorage, sessionStorage, and memory\nexport type ISyncStore<T> = {\n // the identifier every record key starts with\n readonly id: string;\n\n // the mechanism the store was created for. isCompatible tells whether it's available\n readonly mechanism: IStoreMechanism;\n\n // whether the mechanism accepts writes, probed on the first call and remembered\n isCompatible(): boolean;\n\n // reads a record, or the root record when recordId is undefined\n get(recordId?: IRecordId): T | undefined;\n\n // writes a record, or the root record when recordId is undefined\n set(recordId: IRecordId, value: T): void;\n\n // deletes a record, or the root record when recordId is undefined\n del(recordId?: IRecordId): void;\n};\n\n// a store whose methods return promises: IndexedDB\nexport type IAsyncStore<T> = {\n // the identifier every record key starts with\n readonly id: string;\n\n // the mechanism the store was created for. isCompatible tells whether it's available\n readonly mechanism: IStoreMechanism;\n\n // whether the mechanism accepts writes, probed on the first call and remembered\n isCompatible(): Promise<boolean>;\n\n // reads a record, or the root record when recordId is undefined\n get(recordId?: IRecordId): Promise<T | undefined>;\n\n // writes a record, or the root record when recordId is undefined\n set(recordId: IRecordId, value: T): Promise<void>;\n\n // deletes a record, or the root record when recordId is undefined\n del(recordId?: IRecordId): Promise<void>;\n};\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Exception } from 'jg-exception';
|
|
2
|
+
import type { IRecordId, IStoreMechanism, IStoreOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the key a record is stored under, the same one browser-keyval-stores used.
|
|
5
|
+
* @param storeId The store's identifier.
|
|
6
|
+
* @param recordId The record's identifier, or undefined for the store's root record.
|
|
7
|
+
* @returns The key.
|
|
8
|
+
*/
|
|
9
|
+
export declare const buildDataKey: (storeId: string, recordId: IRecordId) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Runs the store's parse option on a stored value, or returns the value as it is without one.
|
|
12
|
+
* @param value The stored value.
|
|
13
|
+
* @param options The store's options.
|
|
14
|
+
* @returns The value.
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseValue: <T>(value: unknown, options: IStoreOptions<T>) => T;
|
|
17
|
+
/**
|
|
18
|
+
* Parses a value read from web storage.
|
|
19
|
+
* @param data The stored text.
|
|
20
|
+
* @returns The value.
|
|
21
|
+
* @throws
|
|
22
|
+
* - FAILED_TO_PARSE_JSON: Unable to parse the JSON data.
|
|
23
|
+
*/
|
|
24
|
+
export declare const parseJSON: (data: string) => unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Serializes a value to be written to web storage.
|
|
27
|
+
* @param value The value.
|
|
28
|
+
* @returns The JSON text.
|
|
29
|
+
* @throws
|
|
30
|
+
* - FAILED_TO_STRINGIFY_JSON: Unable to stringify the JSON data.
|
|
31
|
+
*/
|
|
32
|
+
export declare const stringifyJSON: (value: unknown) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Builds the error a store without a fallback throws when it writes to an unavailable mechanism.
|
|
35
|
+
* @param storeId The store's identifier.
|
|
36
|
+
* @param mechanism The mechanism that's unavailable.
|
|
37
|
+
* @returns The error.
|
|
38
|
+
*/
|
|
39
|
+
export declare const buildUnavailableException: (storeId: string, mechanism: IStoreMechanism) => Exception;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Exception, extractMessage } from 'jg-exception';
|
|
2
|
+
import { ERRORS } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the key a record is stored under, the same one browser-keyval-stores used.
|
|
5
|
+
* @param storeId The store's identifier.
|
|
6
|
+
* @param recordId The record's identifier, or undefined for the store's root record.
|
|
7
|
+
* @returns The key.
|
|
8
|
+
*/
|
|
9
|
+
export const buildDataKey = (storeId, recordId) => recordId === undefined ? storeId : `${storeId}_${recordId}`;
|
|
10
|
+
/**
|
|
11
|
+
* Runs the store's parse option on a stored value, or returns the value as it is without one.
|
|
12
|
+
* @param value The stored value.
|
|
13
|
+
* @param options The store's options.
|
|
14
|
+
* @returns The value.
|
|
15
|
+
*/
|
|
16
|
+
export const parseValue = (value, options) => options.parse === undefined ? value : options.parse(value);
|
|
17
|
+
/**
|
|
18
|
+
* Parses a value read from web storage.
|
|
19
|
+
* @param data The stored text.
|
|
20
|
+
* @returns The value.
|
|
21
|
+
* @throws
|
|
22
|
+
* - FAILED_TO_PARSE_JSON: Unable to parse the JSON data.
|
|
23
|
+
*/
|
|
24
|
+
export const parseJSON = (data) => {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(data);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Exception(`Unable to parse the JSON data: ${extractMessage(error)}`, ERRORS.FAILED_TO_PARSE_JSON, undefined, { cause: error });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Serializes a value to be written to web storage.
|
|
34
|
+
* @param value The value.
|
|
35
|
+
* @returns The JSON text.
|
|
36
|
+
* @throws
|
|
37
|
+
* - FAILED_TO_STRINGIFY_JSON: Unable to stringify the JSON data.
|
|
38
|
+
*/
|
|
39
|
+
export const stringifyJSON = (value) => {
|
|
40
|
+
let data;
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Exception(`Unable to stringify the JSON data: ${extractMessage(error)}`, ERRORS.FAILED_TO_STRINGIFY_JSON, undefined, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
// JSON.stringify returns undefined for undefined, functions, and symbols
|
|
48
|
+
if (data === undefined) {
|
|
49
|
+
throw new Exception(`Unable to stringify the JSON data: JSON can't represent a value of type '${typeof value}'.`, ERRORS.FAILED_TO_STRINGIFY_JSON);
|
|
50
|
+
}
|
|
51
|
+
return data;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Builds the error a store without a fallback throws when it writes to an unavailable mechanism.
|
|
55
|
+
* @param storeId The store's identifier.
|
|
56
|
+
* @param mechanism The mechanism that's unavailable.
|
|
57
|
+
* @returns The error.
|
|
58
|
+
*/
|
|
59
|
+
export const buildUnavailableException = (storeId, mechanism) => new Exception(`The store '${storeId}' can't write because ${mechanism} is unavailable. Pass { fallback: 'memory' } to keep its records in memory instead.`, ERRORS.STORE_UNAVAILABLE);
|
|
60
|
+
//# sourceMappingURL=utilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utilities.js","sourceRoot":"","sources":["../../src/shared/utilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAE,QAAmB,EAAU,EAAE,CAC3E,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,QAAQ,EAAE,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAI,KAAc,EAAE,OAAyB,EAAK,EAAE,CAC5E,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAE,KAAW,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAY,EAAW,EAAE;IACjD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CACjB,kCAAkC,cAAc,CAAC,KAAK,CAAC,EAAE,EACzD,MAAM,CAAC,oBAAoB,EAC3B,SAAS,EACT,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE;IACtD,IAAI,IAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CACjB,sCAAsC,cAAc,CAAC,KAAK,CAAC,EAAE,EAC7D,MAAM,CAAC,wBAAwB,EAC/B,SAAS,EACT,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,SAAS,CACjB,4EAA4E,OAAO,KAAK,IAAI,EAC5F,MAAM,CAAC,wBAAwB,CAChC,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAe,EAAE,SAA0B,EAAa,EAAE,CAClG,IAAI,SAAS,CACX,cAAc,OAAO,yBAAyB,SAAS,qFAAqF,EAC5I,MAAM,CAAC,iBAAiB,CACzB,CAAC","sourcesContent":["import { Exception, extractMessage } from 'jg-exception';\n\nimport { ERRORS } from './errors.js';\nimport type { IRecordId, IStoreMechanism, IStoreOptions } from './types.js';\n\n/**\n * Builds the key a record is stored under, the same one browser-keyval-stores used.\n * @param storeId The store's identifier.\n * @param recordId The record's identifier, or undefined for the store's root record.\n * @returns The key.\n */\nexport const buildDataKey = (storeId: string, recordId: IRecordId): string =>\n recordId === undefined ? storeId : `${storeId}_${recordId}`;\n\n/**\n * Runs the store's parse option on a stored value, or returns the value as it is without one.\n * @param value The stored value.\n * @param options The store's options.\n * @returns The value.\n */\nexport const parseValue = <T>(value: unknown, options: IStoreOptions<T>): T =>\n options.parse === undefined ? (value as T) : options.parse(value);\n\n/**\n * Parses a value read from web storage.\n * @param data The stored text.\n * @returns The value.\n * @throws\n * - FAILED_TO_PARSE_JSON: Unable to parse the JSON data.\n */\nexport const parseJSON = (data: string): unknown => {\n try {\n return JSON.parse(data);\n } catch (error) {\n throw new Exception(\n `Unable to parse the JSON data: ${extractMessage(error)}`,\n ERRORS.FAILED_TO_PARSE_JSON,\n undefined,\n { cause: error },\n );\n }\n};\n\n/**\n * Serializes a value to be written to web storage.\n * @param value The value.\n * @returns The JSON text.\n * @throws\n * - FAILED_TO_STRINGIFY_JSON: Unable to stringify the JSON data.\n */\nexport const stringifyJSON = (value: unknown): string => {\n let data: string | undefined;\n try {\n data = JSON.stringify(value);\n } catch (error) {\n throw new Exception(\n `Unable to stringify the JSON data: ${extractMessage(error)}`,\n ERRORS.FAILED_TO_STRINGIFY_JSON,\n undefined,\n { cause: error },\n );\n }\n\n // JSON.stringify returns undefined for undefined, functions, and symbols\n if (data === undefined) {\n throw new Exception(\n `Unable to stringify the JSON data: JSON can't represent a value of type '${typeof value}'.`,\n ERRORS.FAILED_TO_STRINGIFY_JSON,\n );\n }\n return data;\n};\n\n/**\n * Builds the error a store without a fallback throws when it writes to an unavailable mechanism.\n * @param storeId The store's identifier.\n * @param mechanism The mechanism that's unavailable.\n * @returns The error.\n */\nexport const buildUnavailableException = (storeId: string, mechanism: IStoreMechanism): Exception =>\n new Exception(\n `The store '${storeId}' can't write because ${mechanism} is unavailable. Pass { fallback: 'memory' } to keep its records in memory instead.`,\n ERRORS.STORE_UNAVAILABLE,\n );\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLocalStore, createSessionStore } from './web-storage-store.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/web-storage-store/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["// web storage stores\nexport { createLocalStore, createSessionStore } from './web-storage-store.js';\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type IStoreOptions, type ISyncStore } from '../shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a store over localStorage, whose records outlive the page. Values are stored as JSON,
|
|
4
|
+
* under the same keys browser-keyval-stores used, so their records read back unchanged.
|
|
5
|
+
* localStorage is read and probed on the first call, never when the store is created.
|
|
6
|
+
* @param id The identifier every record key starts with.
|
|
7
|
+
* @param options fallback: 'memory' keeps records in memory when localStorage is unavailable, and
|
|
8
|
+
* parse runs on every stored value get reads.
|
|
9
|
+
* @returns The store.
|
|
10
|
+
* @example
|
|
11
|
+
* const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');
|
|
12
|
+
* settings.set(undefined, { theme: 'dark' }); // stored under the key 'settings'
|
|
13
|
+
* settings.get(); // { theme: 'dark' }
|
|
14
|
+
* settings.set('draft', { theme: 'light' }); // stored under the key 'settings_draft'
|
|
15
|
+
* @throws
|
|
16
|
+
* - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.
|
|
17
|
+
* - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.
|
|
18
|
+
* - STORE_UNAVAILABLE: set or del ran while localStorage is unavailable, without a fallback.
|
|
19
|
+
*
|
|
20
|
+
* Errors from localStorage itself, such as a full quota, propagate.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createLocalStore: <T>(id: string, options?: IStoreOptions<T>) => ISyncStore<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a store over sessionStorage, whose records last as long as the browser tab. Values are
|
|
25
|
+
* stored as JSON, under the same keys browser-keyval-stores used, so their records read back
|
|
26
|
+
* unchanged. sessionStorage is read and probed on the first call, never when the store is created.
|
|
27
|
+
* @param id The identifier every record key starts with.
|
|
28
|
+
* @param options fallback: 'memory' keeps records in memory when sessionStorage is unavailable,
|
|
29
|
+
* and parse runs on every stored value get reads.
|
|
30
|
+
* @returns The store.
|
|
31
|
+
* @example
|
|
32
|
+
* const wizard = createSessionStore<number>('wizard-step', { fallback: 'memory' });
|
|
33
|
+
* wizard.set(undefined, 2);
|
|
34
|
+
* wizard.get(); // 2
|
|
35
|
+
* wizard.del();
|
|
36
|
+
* @throws
|
|
37
|
+
* - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.
|
|
38
|
+
* - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.
|
|
39
|
+
* - STORE_UNAVAILABLE: set or del ran while sessionStorage is unavailable, without a fallback.
|
|
40
|
+
*
|
|
41
|
+
* Errors from sessionStorage itself, such as a full quota, propagate.
|
|
42
|
+
*/
|
|
43
|
+
export declare const createSessionStore: <T>(id: string, options?: IStoreOptions<T>) => ISyncStore<T>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createMemoryStore } from '../memory-store/index.js';
|
|
2
|
+
import { STORAGE_TEST_DATA, buildDataKey, buildUnavailableException, parseJSON, parseValue, stringifyJSON, } from '../shared/index.js';
|
|
3
|
+
const webStorageGlobals = globalThis;
|
|
4
|
+
/**
|
|
5
|
+
* Reads a web storage mechanism and probes whether it accepts writes. Reading it can throw, such
|
|
6
|
+
* as when a browser blocks storage for the page.
|
|
7
|
+
* @param mechanism The mechanism.
|
|
8
|
+
* @returns The storage, or undefined when it's missing or rejects writes.
|
|
9
|
+
*/
|
|
10
|
+
const probeWebStorage = (mechanism) => {
|
|
11
|
+
try {
|
|
12
|
+
const storage = webStorageGlobals[mechanism];
|
|
13
|
+
if (!storage) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
storage.setItem(STORAGE_TEST_DATA.key, STORAGE_TEST_DATA.value);
|
|
17
|
+
storage.removeItem(STORAGE_TEST_DATA.key);
|
|
18
|
+
return storage;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Creates a store over localStorage or sessionStorage. The mechanism is probed on the first call,
|
|
26
|
+
* never when the store is created.
|
|
27
|
+
* @param id The identifier every record key starts with.
|
|
28
|
+
* @param mechanism The mechanism.
|
|
29
|
+
* @param options The store's options.
|
|
30
|
+
* @returns The store.
|
|
31
|
+
*/
|
|
32
|
+
const createWebStorageStore = (id, mechanism, options) => {
|
|
33
|
+
let isProbed = false;
|
|
34
|
+
let storage;
|
|
35
|
+
let memory;
|
|
36
|
+
// the mechanism, probed on the first call. undefined when it's unavailable
|
|
37
|
+
const getStorage = () => {
|
|
38
|
+
if (!isProbed) {
|
|
39
|
+
storage = probeWebStorage(mechanism);
|
|
40
|
+
isProbed = true;
|
|
41
|
+
}
|
|
42
|
+
return storage;
|
|
43
|
+
};
|
|
44
|
+
// the memory store an unavailable mechanism falls back to, when the options ask for one
|
|
45
|
+
const getMemory = () => {
|
|
46
|
+
if (options.fallback === 'memory') {
|
|
47
|
+
memory ??= createMemoryStore(id, options);
|
|
48
|
+
}
|
|
49
|
+
return memory;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
mechanism,
|
|
54
|
+
isCompatible: () => getStorage() !== undefined,
|
|
55
|
+
get: (recordId) => {
|
|
56
|
+
const webStorage = getStorage();
|
|
57
|
+
if (webStorage === undefined) {
|
|
58
|
+
return getMemory()?.get(recordId);
|
|
59
|
+
}
|
|
60
|
+
// a missing record reads as null. An empty string isn't JSON, so it reads as missing too
|
|
61
|
+
const data = webStorage.getItem(buildDataKey(id, recordId));
|
|
62
|
+
return data ? parseValue(parseJSON(data), options) : undefined;
|
|
63
|
+
},
|
|
64
|
+
set: (recordId, value) => {
|
|
65
|
+
const webStorage = getStorage();
|
|
66
|
+
if (webStorage !== undefined) {
|
|
67
|
+
webStorage.setItem(buildDataKey(id, recordId), stringifyJSON(value));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const fallback = getMemory();
|
|
71
|
+
if (fallback === undefined) {
|
|
72
|
+
throw buildUnavailableException(id, mechanism);
|
|
73
|
+
}
|
|
74
|
+
fallback.set(recordId, value);
|
|
75
|
+
},
|
|
76
|
+
del: (recordId) => {
|
|
77
|
+
const webStorage = getStorage();
|
|
78
|
+
if (webStorage !== undefined) {
|
|
79
|
+
webStorage.removeItem(buildDataKey(id, recordId));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const fallback = getMemory();
|
|
83
|
+
if (fallback === undefined) {
|
|
84
|
+
throw buildUnavailableException(id, mechanism);
|
|
85
|
+
}
|
|
86
|
+
fallback.del(recordId);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Creates a store over localStorage, whose records outlive the page. Values are stored as JSON,
|
|
92
|
+
* under the same keys browser-keyval-stores used, so their records read back unchanged.
|
|
93
|
+
* localStorage is read and probed on the first call, never when the store is created.
|
|
94
|
+
* @param id The identifier every record key starts with.
|
|
95
|
+
* @param options fallback: 'memory' keeps records in memory when localStorage is unavailable, and
|
|
96
|
+
* parse runs on every stored value get reads.
|
|
97
|
+
* @returns The store.
|
|
98
|
+
* @example
|
|
99
|
+
* const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');
|
|
100
|
+
* settings.set(undefined, { theme: 'dark' }); // stored under the key 'settings'
|
|
101
|
+
* settings.get(); // { theme: 'dark' }
|
|
102
|
+
* settings.set('draft', { theme: 'light' }); // stored under the key 'settings_draft'
|
|
103
|
+
* @throws
|
|
104
|
+
* - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.
|
|
105
|
+
* - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.
|
|
106
|
+
* - STORE_UNAVAILABLE: set or del ran while localStorage is unavailable, without a fallback.
|
|
107
|
+
*
|
|
108
|
+
* Errors from localStorage itself, such as a full quota, propagate.
|
|
109
|
+
*/
|
|
110
|
+
export const createLocalStore = (id, options = {}) => createWebStorageStore(id, 'localStorage', options);
|
|
111
|
+
/**
|
|
112
|
+
* Creates a store over sessionStorage, whose records last as long as the browser tab. Values are
|
|
113
|
+
* stored as JSON, under the same keys browser-keyval-stores used, so their records read back
|
|
114
|
+
* unchanged. sessionStorage is read and probed on the first call, never when the store is created.
|
|
115
|
+
* @param id The identifier every record key starts with.
|
|
116
|
+
* @param options fallback: 'memory' keeps records in memory when sessionStorage is unavailable,
|
|
117
|
+
* and parse runs on every stored value get reads.
|
|
118
|
+
* @returns The store.
|
|
119
|
+
* @example
|
|
120
|
+
* const wizard = createSessionStore<number>('wizard-step', { fallback: 'memory' });
|
|
121
|
+
* wizard.set(undefined, 2);
|
|
122
|
+
* wizard.get(); // 2
|
|
123
|
+
* wizard.del();
|
|
124
|
+
* @throws
|
|
125
|
+
* - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.
|
|
126
|
+
* - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.
|
|
127
|
+
* - STORE_UNAVAILABLE: set or del ran while sessionStorage is unavailable, without a fallback.
|
|
128
|
+
*
|
|
129
|
+
* Errors from sessionStorage itself, such as a full quota, propagate.
|
|
130
|
+
*/
|
|
131
|
+
export const createSessionStore = (id, options = {}) => createWebStorageStore(id, 'sessionStorage', options);
|
|
132
|
+
//# sourceMappingURL=web-storage-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-storage-store.js","sourceRoot":"","sources":["../../src/web-storage-store/web-storage-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createMemoryStore } from '../memory-store/index.js';\nimport {\n STORAGE_TEST_DATA,\n buildDataKey,\n buildUnavailableException,\n parseJSON,\n parseValue,\n stringifyJSON,\n type IRecordId,\n type IStoreOptions,\n type ISyncStore,\n} from '../shared/index.js';\n\n// the web storage mechanisms, read from globalThis when a store first needs them. Workers have\n// neither, so they're optional here\ntype IWebStorageMechanism = 'localStorage' | 'sessionStorage';\nconst webStorageGlobals: Partial<Record<IWebStorageMechanism, Storage>> = globalThis;\n\n/**\n * Reads a web storage mechanism and probes whether it accepts writes. Reading it can throw, such\n * as when a browser blocks storage for the page.\n * @param mechanism The mechanism.\n * @returns The storage, or undefined when it's missing or rejects writes.\n */\nconst probeWebStorage = (mechanism: IWebStorageMechanism): Storage | undefined => {\n try {\n const storage = webStorageGlobals[mechanism];\n if (!storage) {\n return undefined;\n }\n storage.setItem(STORAGE_TEST_DATA.key, STORAGE_TEST_DATA.value);\n storage.removeItem(STORAGE_TEST_DATA.key);\n return storage;\n } catch {\n return undefined;\n }\n};\n\n/**\n * Creates a store over localStorage or sessionStorage. The mechanism is probed on the first call,\n * never when the store is created.\n * @param id The identifier every record key starts with.\n * @param mechanism The mechanism.\n * @param options The store's options.\n * @returns The store.\n */\nconst createWebStorageStore = <T>(\n id: string,\n mechanism: IWebStorageMechanism,\n options: IStoreOptions<T>,\n): ISyncStore<T> => {\n let isProbed = false;\n let storage: Storage | undefined;\n let memory: ISyncStore<T> | undefined;\n\n // the mechanism, probed on the first call. undefined when it's unavailable\n const getStorage = (): Storage | undefined => {\n if (!isProbed) {\n storage = probeWebStorage(mechanism);\n isProbed = true;\n }\n return storage;\n };\n\n // the memory store an unavailable mechanism falls back to, when the options ask for one\n const getMemory = (): ISyncStore<T> | undefined => {\n if (options.fallback === 'memory') {\n memory ??= createMemoryStore(id, options);\n }\n return memory;\n };\n\n return {\n id,\n mechanism,\n isCompatible: () => getStorage() !== undefined,\n get: (recordId?: IRecordId) => {\n const webStorage = getStorage();\n if (webStorage === undefined) {\n return getMemory()?.get(recordId);\n }\n\n // a missing record reads as null. An empty string isn't JSON, so it reads as missing too\n const data = webStorage.getItem(buildDataKey(id, recordId));\n return data ? parseValue(parseJSON(data), options) : undefined;\n },\n set: (recordId: IRecordId, value: T) => {\n const webStorage = getStorage();\n if (webStorage !== undefined) {\n webStorage.setItem(buildDataKey(id, recordId), stringifyJSON(value));\n return;\n }\n const fallback = getMemory();\n if (fallback === undefined) {\n throw buildUnavailableException(id, mechanism);\n }\n fallback.set(recordId, value);\n },\n del: (recordId?: IRecordId) => {\n const webStorage = getStorage();\n if (webStorage !== undefined) {\n webStorage.removeItem(buildDataKey(id, recordId));\n return;\n }\n const fallback = getMemory();\n if (fallback === undefined) {\n throw buildUnavailableException(id, mechanism);\n }\n fallback.del(recordId);\n },\n };\n};\n\n/**\n * Creates a store over localStorage, whose records outlive the page. Values are stored as JSON,\n * under the same keys browser-keyval-stores used, so their records read back unchanged.\n * localStorage is read and probed on the first call, never when the store is created.\n * @param id The identifier every record key starts with.\n * @param options fallback: 'memory' keeps records in memory when localStorage is unavailable, and\n * parse runs on every stored value get reads.\n * @returns The store.\n * @example\n * const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');\n * settings.set(undefined, { theme: 'dark' }); // stored under the key 'settings'\n * settings.get(); // { theme: 'dark' }\n * settings.set('draft', { theme: 'light' }); // stored under the key 'settings_draft'\n * @throws\n * - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.\n * - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.\n * - STORE_UNAVAILABLE: set or del ran while localStorage is unavailable, without a fallback.\n *\n * Errors from localStorage itself, such as a full quota, propagate.\n */\nexport const createLocalStore = <T>(id: string, options: IStoreOptions<T> = {}): ISyncStore<T> =>\n createWebStorageStore(id, 'localStorage', options);\n\n/**\n * Creates a store over sessionStorage, whose records last as long as the browser tab. Values are\n * stored as JSON, under the same keys browser-keyval-stores used, so their records read back\n * unchanged. sessionStorage is read and probed on the first call, never when the store is created.\n * @param id The identifier every record key starts with.\n * @param options fallback: 'memory' keeps records in memory when sessionStorage is unavailable,\n * and parse runs on every stored value get reads.\n * @returns The store.\n * @example\n * const wizard = createSessionStore<number>('wizard-step', { fallback: 'memory' });\n * wizard.set(undefined, 2);\n * wizard.get(); // 2\n * wizard.del();\n * @throws\n * - FAILED_TO_STRINGIFY_JSON: set received a value JSON can't represent.\n * - FAILED_TO_PARSE_JSON: get read a value that isn't JSON.\n * - STORE_UNAVAILABLE: set or del ran while sessionStorage is unavailable, without a fallback.\n *\n * Errors from sessionStorage itself, such as a full quota, propagate.\n */\nexport const createSessionStore = <T>(id: string, options: IStoreOptions<T> = {}): ISyncStore<T> =>\n createWebStorageStore(id, 'sessionStorage', options);\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jg-storage",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typed key-value stores over localStorage, sessionStorage, IndexedDB, and memory, for browsers and workers.",
|
|
5
|
+
"author": "Jesus Graterol",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/jesusgraterol/jg-packages.git",
|
|
30
|
+
"directory": "packages/jg-storage"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://jg-packages.jesusgraterol.dev/packages/jg-storage/",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/jesusgraterol/jg-packages/issues"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"browser",
|
|
38
|
+
"storage",
|
|
39
|
+
"localstorage",
|
|
40
|
+
"sessionstorage",
|
|
41
|
+
"indexeddb",
|
|
42
|
+
"key-value"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"idb-keyval": "^6.3.0",
|
|
46
|
+
"jg-exception": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@jg-packages/eslint-config": "0.0.0",
|
|
50
|
+
"@jg-packages/tsconfig": "0.0.0",
|
|
51
|
+
"@jg-packages/vitest-config": "0.0.0",
|
|
52
|
+
"@types/node": "24.19.2",
|
|
53
|
+
"browser-keyval-stores": "1.1.1",
|
|
54
|
+
"eslint": "10.12.0",
|
|
55
|
+
"fake-indexeddb": "6.2.5",
|
|
56
|
+
"typescript": "6.0.3",
|
|
57
|
+
"vitest": "5.0.5"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "node ../../internal/build-tools/src/clean-dist.ts && tsc -p tsconfig.build.json",
|
|
61
|
+
"lint": "eslint .",
|
|
62
|
+
"test": "pnpm test:unit && pnpm test:integration",
|
|
63
|
+
"test:integration": "vitest run --project integration",
|
|
64
|
+
"test:unit": "vitest run --project unit",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.browser.json"
|
|
66
|
+
}
|
|
6
67
|
}
|