asljs-dali 0.1.1
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/LICENSE.md +21 -0
- package/README.md +85 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.js +48 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/keys.d.ts +11 -0
- package/dist/keys.js +90 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.js +226 -0
- package/dist/transactions.d.ts +21 -0
- package/dist/transactions.js +85 -0
- package/dist/version-conflict-error.d.ts +6 -0
- package/dist/version-conflict-error.js +9 -0
- package/dist/version-strategy-increment.d.ts +9 -0
- package/dist/version-strategy-increment.js +27 -0
- package/dist/version-strategy-uuid.d.ts +9 -0
- package/dist/version-strategy-uuid.js +23 -0
- package/dist/version-strategy.d.ts +6 -0
- package/dist/version-strategy.js +1 -0
- package/package.json +55 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd
|
|
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
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# dali
|
|
2
|
+
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality,
|
|
4
|
+
performant JavaScript libraries for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-dali` is a data layer for apps that store data in IndexedDB. It is for
|
|
9
|
+
developers who want a typed, event-aware table abstraction instead of
|
|
10
|
+
hand-writing low-level request and transaction plumbing. Use it to model
|
|
11
|
+
stores as `Table<T>`, keep CRUD operations consistent, and optionally enforce
|
|
12
|
+
optimistic concurrency with version strategies.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install asljs-dali
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
NPM Package: [asljs-dali](https://www.npmjs.com/package/asljs-dali)
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
dbOpen,
|
|
27
|
+
Table,
|
|
28
|
+
} from 'asljs-dali';
|
|
29
|
+
|
|
30
|
+
type Note =
|
|
31
|
+
{ id: string;
|
|
32
|
+
title: string; };
|
|
33
|
+
|
|
34
|
+
const db =
|
|
35
|
+
await dbOpen(
|
|
36
|
+
'notes-db',
|
|
37
|
+
[ targetDb => {
|
|
38
|
+
targetDb.createObjectStore(
|
|
39
|
+
'notes',
|
|
40
|
+
{ keyPath: 'id' });
|
|
41
|
+
} ]);
|
|
42
|
+
|
|
43
|
+
const notes =
|
|
44
|
+
new Table<Note>(
|
|
45
|
+
'notes',
|
|
46
|
+
db);
|
|
47
|
+
|
|
48
|
+
await notes.add(
|
|
49
|
+
{ id: '1',
|
|
50
|
+
title: 'Hello' });
|
|
51
|
+
|
|
52
|
+
const row =
|
|
53
|
+
await notes.getOne('1');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API Reference
|
|
57
|
+
|
|
58
|
+
Core:
|
|
59
|
+
|
|
60
|
+
- `dbOpen(name, upgrades)`
|
|
61
|
+
- `dbDelete(name)`
|
|
62
|
+
- `dbRequestAsync(request)`
|
|
63
|
+
- `Table<T>`
|
|
64
|
+
|
|
65
|
+
Versioning:
|
|
66
|
+
|
|
67
|
+
- `TableVersionStrategy<T>`
|
|
68
|
+
- `TableVersionConflictError`
|
|
69
|
+
- `IncrementTableVersionStrategy<T>`
|
|
70
|
+
- `UuidTableVersionStrategy<T>`
|
|
71
|
+
|
|
72
|
+
Transactions:
|
|
73
|
+
|
|
74
|
+
- `TxMode`
|
|
75
|
+
- `txRead(db, storeName, tx?)`
|
|
76
|
+
- `txWrite(db, storeName, tx?)`
|
|
77
|
+
- `txDone(tx)`
|
|
78
|
+
- `txEnsure(tx, storeName, mode)`
|
|
79
|
+
- `txReuseOrCreate(tx, storeNames, mode, db)`
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT License. See [LICENSE](LICENSE.md) for details.
|
|
84
|
+
|
|
85
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
package/dist/db.d.ts
ADDED
package/dist/db.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function dbRequestAsync(request) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
request.addEventListener('success', () => {
|
|
4
|
+
resolve(request.result);
|
|
5
|
+
});
|
|
6
|
+
request.addEventListener('error', () => {
|
|
7
|
+
reject(request.error
|
|
8
|
+
?? new Error('IndexedDB request failed'));
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function dbOpen(name, upgrades) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const request = indexedDB.open(name, upgrades.length);
|
|
15
|
+
request.addEventListener('upgradeneeded', e => {
|
|
16
|
+
const updates = upgrades.slice(e.oldVersion - 1, e.newVersion
|
|
17
|
+
?? upgrades.length - 1);
|
|
18
|
+
for (const update of updates) {
|
|
19
|
+
update(request.result);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
request.addEventListener('success', () => {
|
|
23
|
+
resolve(request.result);
|
|
24
|
+
});
|
|
25
|
+
request.addEventListener('blocked', () => {
|
|
26
|
+
reject(new Error('Database opening is blocked'));
|
|
27
|
+
});
|
|
28
|
+
request.addEventListener('error', () => {
|
|
29
|
+
reject(request.error
|
|
30
|
+
?? new Error('Failed to open database'));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function dbDelete(name) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const request = indexedDB.deleteDatabase(name);
|
|
37
|
+
request.addEventListener('success', () => {
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
request.addEventListener('blocked', () => {
|
|
41
|
+
reject(new Error('Database deletion is blocked'));
|
|
42
|
+
});
|
|
43
|
+
request.addEventListener('error', () => {
|
|
44
|
+
reject(request.error
|
|
45
|
+
?? new Error('Failed to delete database'));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { dbDelete, dbOpen, dbRequestAsync, } from './db.js';
|
|
2
|
+
export { IncrementVersionStrategy as IncrementTableVersionStrategy, } from './version-strategy-increment.js';
|
|
3
|
+
export { Table, type TableEvents, type TableEventsReceiver, } from './table.js';
|
|
4
|
+
export { VersionConflictError as TableVersionConflictError, } from './version-conflict-error.js';
|
|
5
|
+
export { type VersionStrategy as TableVersionStrategy, } from './version-strategy.js';
|
|
6
|
+
export { txDone, txRead, txReuseOrCreate, txWrite, TxMode, txEnsure, } from './transactions.js';
|
|
7
|
+
export { UuidVersionStrategy as UuidTableVersionStrategy, } from './version-strategy-uuid.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { dbDelete, dbOpen, dbRequestAsync, } from './db.js';
|
|
2
|
+
export { IncrementVersionStrategy as IncrementTableVersionStrategy, } from './version-strategy-increment.js';
|
|
3
|
+
export { Table, } from './table.js';
|
|
4
|
+
export { VersionConflictError as TableVersionConflictError, } from './version-conflict-error.js';
|
|
5
|
+
export { txDone, txRead, txReuseOrCreate, txWrite, TxMode, txEnsure, } from './transactions.js';
|
|
6
|
+
export { UuidVersionStrategy as UuidTableVersionStrategy, } from './version-strategy-uuid.js';
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type KeyRecord = {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
};
|
|
4
|
+
export type KeyPath<R extends KeyRecord> = (string & keyof R)[] | (string & keyof R);
|
|
5
|
+
export declare function keyPathValid(keyPath: string | string[]): boolean;
|
|
6
|
+
export declare function keyPathAssert(keyPath: string | string[]): asserts keyPath is string | string[];
|
|
7
|
+
export declare function keyValueValid(value: any): boolean;
|
|
8
|
+
export declare function keyValueAssert(value: any): asserts value is IDBValidKey;
|
|
9
|
+
export declare function keyValid(keyPath: string | string[], key: IDBValidKey): boolean;
|
|
10
|
+
export declare function keyAssert(keyPath: string | string[], key: IDBValidKey): asserts key is IDBValidKey;
|
|
11
|
+
export declare function keyGet<R extends KeyRecord>(keyPath: KeyPath<R>, record: R): IDBValidKey;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function keyPathItemValid(keyPathItem) {
|
|
2
|
+
return typeof keyPathItem === 'string'
|
|
3
|
+
&& keyPathItem.length > 0;
|
|
4
|
+
}
|
|
5
|
+
export function keyPathValid(keyPath) {
|
|
6
|
+
if (Array.isArray(keyPath)) {
|
|
7
|
+
if (keyPath.length < 1)
|
|
8
|
+
return false;
|
|
9
|
+
return keyPath.every(keyPathItemValid);
|
|
10
|
+
}
|
|
11
|
+
return keyPathItemValid(keyPath);
|
|
12
|
+
}
|
|
13
|
+
export function keyPathAssert(keyPath) {
|
|
14
|
+
if (!keyPathValid(keyPath)) {
|
|
15
|
+
throw new TypeError('Key path must be a non-empty string or an array of non-empty strings.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function keyValueValid(value) {
|
|
19
|
+
if (value === null
|
|
20
|
+
|| value === undefined) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return true;
|
|
25
|
+
if (typeof value === 'number')
|
|
26
|
+
return Number.isFinite(value);
|
|
27
|
+
if (value instanceof Date)
|
|
28
|
+
return !Number.isNaN(value.getTime());
|
|
29
|
+
if (value instanceof ArrayBuffer)
|
|
30
|
+
return true;
|
|
31
|
+
if (ArrayBuffer.isView(value))
|
|
32
|
+
return true;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (let i = 0; i < value.length; i++) {
|
|
35
|
+
if (!(i in value))
|
|
36
|
+
return false;
|
|
37
|
+
if (!keyValueValid(value[i]))
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
export function keyValueAssert(value) {
|
|
45
|
+
if (!keyValueValid(value)) {
|
|
46
|
+
throw new TypeError('Value is not a valid IndexedDB key value.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function keyValid(keyPath, key) {
|
|
50
|
+
if (!Array.isArray(keyPath)) {
|
|
51
|
+
return !Array.isArray(key)
|
|
52
|
+
&& keyValueValid(key);
|
|
53
|
+
}
|
|
54
|
+
if (keyPath.length === 1) {
|
|
55
|
+
return !Array.isArray(key)
|
|
56
|
+
&& keyValueValid(key);
|
|
57
|
+
}
|
|
58
|
+
return Array.isArray(key)
|
|
59
|
+
&& key.length === keyPath.length
|
|
60
|
+
&& key.every(keyValueValid);
|
|
61
|
+
}
|
|
62
|
+
export function keyAssert(keyPath, key) {
|
|
63
|
+
const keyPathLength = Array.isArray(keyPath)
|
|
64
|
+
? keyPath.length
|
|
65
|
+
: 1;
|
|
66
|
+
if (!keyValid(keyPath, key)) {
|
|
67
|
+
throw new TypeError(keyPathLength === 1
|
|
68
|
+
? 'Key must be a single value.'
|
|
69
|
+
: `Key must be an array of length ${keyPathLength}.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function keyGet(keyPath, record) {
|
|
73
|
+
if (!Array.isArray(keyPath)) {
|
|
74
|
+
const keyValue = record[keyPath];
|
|
75
|
+
keyValueAssert(keyValue);
|
|
76
|
+
return keyValue;
|
|
77
|
+
}
|
|
78
|
+
if (keyPath.length === 1) {
|
|
79
|
+
const keyValue = record[keyPath[0]];
|
|
80
|
+
keyValueAssert(keyValue);
|
|
81
|
+
return keyValue;
|
|
82
|
+
}
|
|
83
|
+
const key = new Array(keyPath.length);
|
|
84
|
+
for (let i = 0; i < keyPath.length; i++) {
|
|
85
|
+
const keyValue = record[keyPath[i]];
|
|
86
|
+
keyValueAssert(keyValue);
|
|
87
|
+
key[i] = keyValue;
|
|
88
|
+
}
|
|
89
|
+
return key;
|
|
90
|
+
}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EventfulBase } from 'asljs-eventful';
|
|
2
|
+
import { type KeyPath } from './keys.js';
|
|
3
|
+
import { type VersionStrategy } from './version-strategy.js';
|
|
4
|
+
export type TableEvents<T extends Record<string, any>> = {
|
|
5
|
+
add: [record: T];
|
|
6
|
+
update: [record: T, previousRecord: T];
|
|
7
|
+
delete: [record: T];
|
|
8
|
+
clear: [records: T[]];
|
|
9
|
+
};
|
|
10
|
+
export type TableEventsReceiver<T extends Record<string, any>> = Partial<{
|
|
11
|
+
[K in keyof TableEvents<T>]: (...args: TableEvents<T>[K]) => void;
|
|
12
|
+
}>;
|
|
13
|
+
export declare class Table<T extends Record<string, any>> extends EventfulBase<TableEvents<T>> {
|
|
14
|
+
#private;
|
|
15
|
+
readonly storeName: string;
|
|
16
|
+
readonly db: IDBDatabase;
|
|
17
|
+
readonly key: KeyPath<T>;
|
|
18
|
+
constructor(storeName: string, db: IDBDatabase, strategy?: VersionStrategy<T>);
|
|
19
|
+
getOne(key: IDBValidKey, tx?: IDBTransaction | null): Promise<T | null>;
|
|
20
|
+
get(index: string, key: IDBValidKey, tx?: IDBTransaction | null): Promise<T[]>;
|
|
21
|
+
notify(receiver: TableEventsReceiver<T>): () => boolean;
|
|
22
|
+
getAll(tx?: IDBTransaction | null): Promise<T[]>;
|
|
23
|
+
scan(predicate: (record: T) => boolean, tx?: IDBTransaction | null): Promise<T[]>;
|
|
24
|
+
add(record: T, tx?: IDBTransaction | null): Promise<T>;
|
|
25
|
+
update(record: T, expectedVersion?: unknown, tx?: IDBTransaction | null): Promise<T>;
|
|
26
|
+
delete(key: IDBValidKey, expectedVersion?: unknown, tx?: IDBTransaction | null): Promise<void>;
|
|
27
|
+
clear(tx?: IDBTransaction | null): Promise<void>;
|
|
28
|
+
}
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _Table_instances, _Table_receivers, _Table_strategy, _Table_onTransactionCompleted, _Table_notify;
|
|
13
|
+
import { EventfulBase } from 'asljs-eventful';
|
|
14
|
+
import { dbRequestAsync } from './db.js';
|
|
15
|
+
import { keyAssert, keyGet, keyPathAssert, } from './keys.js';
|
|
16
|
+
import { VersionConflictError } from './version-conflict-error.js';
|
|
17
|
+
import { txDone, txRead, txWrite, } from './transactions.js';
|
|
18
|
+
export class Table extends EventfulBase {
|
|
19
|
+
constructor(storeName, db, strategy) {
|
|
20
|
+
super();
|
|
21
|
+
_Table_instances.add(this);
|
|
22
|
+
this.storeName = storeName;
|
|
23
|
+
this.db = db;
|
|
24
|
+
_Table_receivers.set(this, []);
|
|
25
|
+
_Table_strategy.set(this, void 0);
|
|
26
|
+
__classPrivateFieldSet(this, _Table_strategy, strategy, "f");
|
|
27
|
+
const key = txRead(this.db, this.storeName)
|
|
28
|
+
.objectStore(this.storeName)
|
|
29
|
+
.keyPath;
|
|
30
|
+
keyPathAssert(key);
|
|
31
|
+
this.key = key;
|
|
32
|
+
}
|
|
33
|
+
getOne(key, tx = null) {
|
|
34
|
+
keyAssert(this.key, key);
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const request = txRead(this.db, this.storeName, tx)
|
|
37
|
+
.objectStore(this.storeName)
|
|
38
|
+
.get(key);
|
|
39
|
+
request.onsuccess =
|
|
40
|
+
() => {
|
|
41
|
+
const fields = request.result;
|
|
42
|
+
if (fields === undefined) {
|
|
43
|
+
resolve(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
resolve(fields);
|
|
47
|
+
};
|
|
48
|
+
request.onerror =
|
|
49
|
+
() => {
|
|
50
|
+
reject(request.error
|
|
51
|
+
?? new Error(`${this.storeName}: getOne request failed`));
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
get(index, key, tx = null) {
|
|
56
|
+
const store = txRead(this.db, this.storeName, tx)
|
|
57
|
+
.objectStore(this.storeName);
|
|
58
|
+
const idx = store.index(index);
|
|
59
|
+
const keyPath = idx.keyPath;
|
|
60
|
+
keyPathAssert(keyPath);
|
|
61
|
+
keyAssert(keyPath, key);
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const request = idx.getAll(key);
|
|
64
|
+
request.onsuccess =
|
|
65
|
+
() => {
|
|
66
|
+
const records = request.result;
|
|
67
|
+
resolve(records);
|
|
68
|
+
};
|
|
69
|
+
request.onerror =
|
|
70
|
+
() => {
|
|
71
|
+
reject(request.error
|
|
72
|
+
?? new Error(`${this.storeName}: get request failed for index ${index}`));
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
notify(receiver) {
|
|
77
|
+
__classPrivateFieldGet(this, _Table_receivers, "f").push(receiver);
|
|
78
|
+
return () => {
|
|
79
|
+
const index = __classPrivateFieldGet(this, _Table_receivers, "f").indexOf(receiver);
|
|
80
|
+
if (index < 0)
|
|
81
|
+
return false;
|
|
82
|
+
__classPrivateFieldGet(this, _Table_receivers, "f").splice(index, 1);
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async getAll(tx = null) {
|
|
87
|
+
const records = await dbRequestAsync(txRead(this.db, this.storeName, tx)
|
|
88
|
+
.objectStore(this.storeName)
|
|
89
|
+
.getAll());
|
|
90
|
+
return records;
|
|
91
|
+
}
|
|
92
|
+
scan(predicate, tx = null) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const request = txRead(this.db, this.storeName, tx)
|
|
95
|
+
.objectStore(this.storeName)
|
|
96
|
+
.openCursor();
|
|
97
|
+
const result = [];
|
|
98
|
+
request.onsuccess =
|
|
99
|
+
() => {
|
|
100
|
+
const cursor = request.result;
|
|
101
|
+
if (cursor === null) {
|
|
102
|
+
resolve(result);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const record = cursor.value;
|
|
106
|
+
try {
|
|
107
|
+
if (predicate(record)) {
|
|
108
|
+
result.push(record);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
reject(error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
cursor.continue();
|
|
116
|
+
};
|
|
117
|
+
request.onerror =
|
|
118
|
+
() => {
|
|
119
|
+
reject(request.error
|
|
120
|
+
?? new Error(`${this.storeName}: scan request failed`));
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async add(record, tx = null) {
|
|
125
|
+
const storedRecord = __classPrivateFieldGet(this, _Table_strategy, "f")
|
|
126
|
+
? __classPrivateFieldGet(this, _Table_strategy, "f").initialise(record)
|
|
127
|
+
: record;
|
|
128
|
+
const ltx = txWrite(this.db, this.storeName, tx);
|
|
129
|
+
const store = ltx.objectStore(this.storeName);
|
|
130
|
+
await dbRequestAsync(store.add(storedRecord));
|
|
131
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
|
|
132
|
+
this.emit('add', storedRecord);
|
|
133
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'add', [storedRecord]);
|
|
134
|
+
});
|
|
135
|
+
if (tx === null)
|
|
136
|
+
await txDone(ltx);
|
|
137
|
+
return storedRecord;
|
|
138
|
+
}
|
|
139
|
+
async update(record, expectedVersion, tx = null) {
|
|
140
|
+
const key = keyGet(this.key, record);
|
|
141
|
+
const ltx = txWrite(this.db, this.storeName, tx);
|
|
142
|
+
const store = ltx.objectStore(this.storeName);
|
|
143
|
+
const existing = await dbRequestAsync(store.get(key));
|
|
144
|
+
if (!existing) {
|
|
145
|
+
throw new Error(`Record with key ${String(key)} not found.`);
|
|
146
|
+
}
|
|
147
|
+
let storedRecord = record;
|
|
148
|
+
if (__classPrivateFieldGet(this, _Table_strategy, "f")) {
|
|
149
|
+
if (expectedVersion === undefined) {
|
|
150
|
+
throw new Error(`${this.storeName}: expectedVersion is required when a version strategy is configured.`);
|
|
151
|
+
}
|
|
152
|
+
if (!__classPrivateFieldGet(this, _Table_strategy, "f").verify(existing, expectedVersion)) {
|
|
153
|
+
throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_strategy, "f").getVersion(existing));
|
|
154
|
+
}
|
|
155
|
+
storedRecord = __classPrivateFieldGet(this, _Table_strategy, "f").update(record);
|
|
156
|
+
}
|
|
157
|
+
await dbRequestAsync(store.put(storedRecord));
|
|
158
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
|
|
159
|
+
this.emit('update', storedRecord, existing);
|
|
160
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'update', [storedRecord,
|
|
161
|
+
existing]);
|
|
162
|
+
});
|
|
163
|
+
if (tx === null)
|
|
164
|
+
await txDone(ltx);
|
|
165
|
+
return storedRecord;
|
|
166
|
+
}
|
|
167
|
+
async delete(key, expectedVersion, tx = null) {
|
|
168
|
+
keyAssert(this.key, key);
|
|
169
|
+
const ltx = txWrite(this.db, this.storeName, tx);
|
|
170
|
+
const store = ltx.objectStore(this.storeName);
|
|
171
|
+
const existing = await dbRequestAsync(store.get(key));
|
|
172
|
+
if (existing === undefined)
|
|
173
|
+
return;
|
|
174
|
+
if (__classPrivateFieldGet(this, _Table_strategy, "f")) {
|
|
175
|
+
if (expectedVersion === undefined) {
|
|
176
|
+
throw new Error(`${this.storeName}: expectedVersion is required when a version strategy is configured.`);
|
|
177
|
+
}
|
|
178
|
+
if (!__classPrivateFieldGet(this, _Table_strategy, "f").verify(existing, expectedVersion)) {
|
|
179
|
+
throw new VersionConflictError(key, expectedVersion, __classPrivateFieldGet(this, _Table_strategy, "f").getVersion(existing));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await dbRequestAsync(store.delete(key));
|
|
183
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
|
|
184
|
+
this.emit('delete', existing);
|
|
185
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'delete', [existing]);
|
|
186
|
+
});
|
|
187
|
+
if (tx === null)
|
|
188
|
+
await txDone(ltx);
|
|
189
|
+
}
|
|
190
|
+
async clear(tx = null) {
|
|
191
|
+
const ltx = txWrite(this.db, this.storeName, tx);
|
|
192
|
+
const store = ltx.objectStore(this.storeName);
|
|
193
|
+
const records = await dbRequestAsync(store.getAll());
|
|
194
|
+
await dbRequestAsync(store.clear());
|
|
195
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_onTransactionCompleted).call(this, store.transaction, () => {
|
|
196
|
+
this.emit('clear', records);
|
|
197
|
+
__classPrivateFieldGet(this, _Table_instances, "m", _Table_notify).call(this, 'clear', [records]);
|
|
198
|
+
});
|
|
199
|
+
if (tx === null)
|
|
200
|
+
await txDone(ltx);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
_Table_receivers = new WeakMap(), _Table_strategy = new WeakMap(), _Table_instances = new WeakSet(), _Table_onTransactionCompleted = function _Table_onTransactionCompleted(tx, action) {
|
|
204
|
+
tx.addEventListener('complete', () => {
|
|
205
|
+
try {
|
|
206
|
+
action();
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error(`${this.storeName}: on complete action failed`, error);
|
|
210
|
+
}
|
|
211
|
+
}, { once: true });
|
|
212
|
+
}, _Table_notify = function _Table_notify(eventName, args) {
|
|
213
|
+
if (__classPrivateFieldGet(this, _Table_receivers, "f").length === 0)
|
|
214
|
+
return;
|
|
215
|
+
for (const receiver of __classPrivateFieldGet(this, _Table_receivers, "f")) {
|
|
216
|
+
const handler = receiver[eventName];
|
|
217
|
+
if (typeof handler !== 'function')
|
|
218
|
+
continue;
|
|
219
|
+
try {
|
|
220
|
+
handler(...args);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error(`${this.storeName}: notify handler failed for event ${String(eventName)}`, error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const TxMode: {
|
|
2
|
+
read: IDBTransactionMode;
|
|
3
|
+
readWrite: IDBTransactionMode;
|
|
4
|
+
};
|
|
5
|
+
export declare function txDone(tx: IDBTransaction): Promise<void>;
|
|
6
|
+
export declare function txReuseOrCreate(tx: IDBTransaction | null, storeNames: string | string[], mode: IDBTransactionMode, db: IDBDatabase): IDBTransaction;
|
|
7
|
+
export declare function txEnsure(tx: IDBTransaction, storeName: string, mode: IDBTransactionMode): void;
|
|
8
|
+
export declare class UnsupportedTransactionModeError extends Error {
|
|
9
|
+
constructor(mode: IDBTransactionMode);
|
|
10
|
+
}
|
|
11
|
+
export declare class TransactionReadModeRequiredError extends Error {
|
|
12
|
+
constructor(storeName: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class TransactionWriteModeRequiredError extends Error {
|
|
15
|
+
constructor(storeName: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class TransactionStoreAccessError extends Error {
|
|
18
|
+
constructor(storeName: string);
|
|
19
|
+
}
|
|
20
|
+
export declare function txRead(db: IDBDatabase, storeName: string, tx?: IDBTransaction | null): IDBTransaction;
|
|
21
|
+
export declare function txWrite(db: IDBDatabase, storeName: string, tx?: IDBTransaction | null): IDBTransaction;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const TxMode = { read: 'readonly',
|
|
2
|
+
readWrite: 'readwrite' };
|
|
3
|
+
export function txDone(tx) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
tx.addEventListener('complete', () => {
|
|
6
|
+
resolve();
|
|
7
|
+
});
|
|
8
|
+
tx.addEventListener('abort', () => {
|
|
9
|
+
reject(tx.error);
|
|
10
|
+
});
|
|
11
|
+
tx.addEventListener('error', () => {
|
|
12
|
+
reject(tx.error);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function txReuseOrCreate(tx, storeNames, mode, db) {
|
|
17
|
+
if (tx) {
|
|
18
|
+
const storeNamesArray = Array.isArray(storeNames)
|
|
19
|
+
? storeNames
|
|
20
|
+
: [storeNames];
|
|
21
|
+
for (const storeName of storeNamesArray) {
|
|
22
|
+
txEnsure(tx, storeName, mode);
|
|
23
|
+
}
|
|
24
|
+
return tx;
|
|
25
|
+
}
|
|
26
|
+
return db.transaction(storeNames, mode);
|
|
27
|
+
}
|
|
28
|
+
export function txEnsure(tx, storeName, mode) {
|
|
29
|
+
if (!tx.objectStoreNames.contains(storeName)) {
|
|
30
|
+
throw new TransactionStoreAccessError(storeName);
|
|
31
|
+
}
|
|
32
|
+
switch (mode) {
|
|
33
|
+
case TxMode.read:
|
|
34
|
+
if (tx.mode !== TxMode.read
|
|
35
|
+
&& tx.mode !== TxMode.readWrite) {
|
|
36
|
+
throw new TransactionReadModeRequiredError(storeName);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case TxMode.readWrite:
|
|
40
|
+
if (tx.mode !== TxMode.readWrite) {
|
|
41
|
+
throw new TransactionWriteModeRequiredError(storeName);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
throw new UnsupportedTransactionModeError(mode);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class UnsupportedTransactionModeError extends Error {
|
|
49
|
+
constructor(mode) {
|
|
50
|
+
super(`Unsupported transaction mode "${mode}"`);
|
|
51
|
+
this.name = 'UnsupportedTransactionModeError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class TransactionReadModeRequiredError extends Error {
|
|
55
|
+
constructor(storeName) {
|
|
56
|
+
super(`Transaction does not have read access to the store "${storeName}".`);
|
|
57
|
+
this.name = 'TransactionReadModeRequiredError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class TransactionWriteModeRequiredError extends Error {
|
|
61
|
+
constructor(storeName) {
|
|
62
|
+
super(`Transaction does not have write access to the store "${storeName}".`);
|
|
63
|
+
this.name = 'TransactionWriteModeRequiredError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class TransactionStoreAccessError extends Error {
|
|
67
|
+
constructor(storeName) {
|
|
68
|
+
super(`Transaction does not have access to the store "${storeName}".`);
|
|
69
|
+
this.name = 'TransactionStoreAccessError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function txRead(db, storeName, tx = null) {
|
|
73
|
+
if (tx !== null) {
|
|
74
|
+
txEnsure(tx, storeName, TxMode.read);
|
|
75
|
+
return tx;
|
|
76
|
+
}
|
|
77
|
+
return db.transaction([storeName], TxMode.read);
|
|
78
|
+
}
|
|
79
|
+
export function txWrite(db, storeName, tx = null) {
|
|
80
|
+
if (tx !== null) {
|
|
81
|
+
txEnsure(tx, storeName, TxMode.readWrite);
|
|
82
|
+
return tx;
|
|
83
|
+
}
|
|
84
|
+
return db.transaction([storeName], TxMode.readWrite);
|
|
85
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class VersionConflictError extends Error {
|
|
2
|
+
constructor(key, expectedVersion, actualVersion) {
|
|
3
|
+
super(`Version conflict for key ${String(key)}.`);
|
|
4
|
+
this.key = key;
|
|
5
|
+
this.expectedVersion = expectedVersion;
|
|
6
|
+
this.actualVersion = actualVersion;
|
|
7
|
+
this.name = 'VersionConflictError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type VersionStrategy } from './version-strategy.js';
|
|
2
|
+
export declare class IncrementVersionStrategy<T extends Record<string, any>> implements VersionStrategy<T> {
|
|
3
|
+
private readonly field;
|
|
4
|
+
constructor(field: string & keyof T);
|
|
5
|
+
getVersion(record: T): unknown;
|
|
6
|
+
initialise(record: T): T;
|
|
7
|
+
verify(record: T, expectedVersion: unknown): boolean;
|
|
8
|
+
update(record: T): T;
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class IncrementVersionStrategy {
|
|
2
|
+
constructor(field) {
|
|
3
|
+
this.field = field;
|
|
4
|
+
}
|
|
5
|
+
getVersion(record) {
|
|
6
|
+
return record[this.field];
|
|
7
|
+
}
|
|
8
|
+
initialise(record) {
|
|
9
|
+
const version = record[this.field];
|
|
10
|
+
if (typeof version === 'number'
|
|
11
|
+
&& Number.isFinite(version)) {
|
|
12
|
+
return record;
|
|
13
|
+
}
|
|
14
|
+
return { ...record, [this.field]: 1 };
|
|
15
|
+
}
|
|
16
|
+
verify(record, expectedVersion) {
|
|
17
|
+
return record[this.field] === expectedVersion;
|
|
18
|
+
}
|
|
19
|
+
update(record) {
|
|
20
|
+
const version = record[this.field];
|
|
21
|
+
if (typeof version !== 'number'
|
|
22
|
+
|| !Number.isFinite(version)) {
|
|
23
|
+
throw new Error(`Version field "${String(this.field)}" does not contain a valid number.`);
|
|
24
|
+
}
|
|
25
|
+
return { ...record, [this.field]: version + 1 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type VersionStrategy } from './version-strategy.js';
|
|
2
|
+
export declare class UuidVersionStrategy<T extends Record<string, any>> implements VersionStrategy<T> {
|
|
3
|
+
private readonly field;
|
|
4
|
+
constructor(field: string & keyof T);
|
|
5
|
+
getVersion(record: T): unknown;
|
|
6
|
+
initialise(record: T): T;
|
|
7
|
+
verify(record: T, expectedVersion: unknown): boolean;
|
|
8
|
+
update(record: T): T;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
|
+
export class UuidVersionStrategy {
|
|
3
|
+
constructor(field) {
|
|
4
|
+
this.field = field;
|
|
5
|
+
}
|
|
6
|
+
getVersion(record) {
|
|
7
|
+
return record[this.field];
|
|
8
|
+
}
|
|
9
|
+
initialise(record) {
|
|
10
|
+
const version = record[this.field];
|
|
11
|
+
if (typeof version === 'string'
|
|
12
|
+
&& UUID_PATTERN.test(version)) {
|
|
13
|
+
return record;
|
|
14
|
+
}
|
|
15
|
+
return { ...record, [this.field]: crypto.randomUUID() };
|
|
16
|
+
}
|
|
17
|
+
verify(record, expectedVersion) {
|
|
18
|
+
return record[this.field] === expectedVersion;
|
|
19
|
+
}
|
|
20
|
+
update(record) {
|
|
21
|
+
return { ...record, [this.field]: crypto.randomUUID() };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "asljs-dali",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "IndexedDB data layer with a typed Table abstraction.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/**",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "node ../toolkit.js clean-dist",
|
|
21
|
+
"build": "npx tsc -p tsconfig.build.json",
|
|
22
|
+
"build:test": "npx tsc -p tsconfig.test.json",
|
|
23
|
+
"lint": "npx eslint .",
|
|
24
|
+
"lint:fix": "npx eslint . --fix",
|
|
25
|
+
"guard:clean-git": "node ../toolkit.js ensure-clean-working-folder",
|
|
26
|
+
"prepack": "npm run clean && npm run build",
|
|
27
|
+
"prepublishOnly": "npm run guard:clean-git && npm run prepack",
|
|
28
|
+
"postpublish": "node ../toolkit.js tag-commit-with-release-id",
|
|
29
|
+
"test": "npm run build:test && node --test dist/*.test.js",
|
|
30
|
+
"test:watch": "npm run build:test && node --watch --test dist/*.test.js",
|
|
31
|
+
"coverage": "npm run build:test && NODE_V8_COVERAGE=.coverage node --test dist/*.test.js && node -e \"console.log('Coverage in .coverage (use c8/istanbul if you want reports)')\""
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"indexeddb",
|
|
35
|
+
"table",
|
|
36
|
+
"data-layer",
|
|
37
|
+
"javascript"
|
|
38
|
+
],
|
|
39
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"asljs-eventful": "^0.4.8"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"fake-indexeddb": "^6.2.4"
|
|
54
|
+
}
|
|
55
|
+
}
|