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 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
@@ -0,0 +1,3 @@
1
+ export declare function dbRequestAsync<T>(request: IDBRequest<T>): Promise<T>;
2
+ export declare function dbOpen(name: string, upgrades: ((db: IDBDatabase) => void)[]): Promise<IDBDatabase>;
3
+ export declare function dbDelete(name: string): Promise<void>;
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
+ }
@@ -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
+ }
@@ -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,6 @@
1
+ export declare class VersionConflictError extends Error {
2
+ readonly key: IDBValidKey;
3
+ readonly expectedVersion: unknown;
4
+ readonly actualVersion: unknown;
5
+ constructor(key: IDBValidKey, expectedVersion: unknown, actualVersion: unknown);
6
+ }
@@ -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,6 @@
1
+ export interface VersionStrategy<T extends Record<string, any>> {
2
+ getVersion(record: T): unknown;
3
+ initialise(record: T): T;
4
+ verify(record: T, expectedVersion: unknown): boolean;
5
+ update(record: T): T;
6
+ }
@@ -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
+ }