@hashtree/dexie 0.1.3 → 0.1.4
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/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -11
- package/dist/index.test.js +22 -0
- package/package.json +8 -2
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ import type { Store, Hash } from '@hashtree/core';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class DexieStore implements Store {
|
|
7
7
|
private db;
|
|
8
|
+
private pendingLastAccessUpdates;
|
|
9
|
+
private lastAccessFlushPromise;
|
|
10
|
+
private lastAccessFlushTimer;
|
|
8
11
|
constructor(dbName?: string);
|
|
12
|
+
private scheduleLastAccessTouch;
|
|
13
|
+
private flushPendingLastAccessUpdates;
|
|
9
14
|
put(hash: Hash, data: Uint8Array): Promise<boolean>;
|
|
10
15
|
get(hash: Hash): Promise<Uint8Array | null>;
|
|
11
16
|
has(hash: Hash): Promise<boolean>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AA4DlD;;;GAGG;AACH,qBAAa,UAAW,YAAW,KAAK;IACtC,OAAO,CAAC,EAAE,CAAa;IACvB,OAAO,CAAC,wBAAwB,CAA6B;IAC7D,OAAO,CAAC,sBAAsB,CAA8B;IAC5D,OAAO,CAAC,oBAAoB,CAA8C;gBAE9D,MAAM,GAAE,MAAmB;IAIvC,OAAO,CAAC,uBAAuB;YAYjB,6BAA6B;IAyCrC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAwB3C,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAYjC,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB1C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;IAW7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAS9B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAanC;;;OAGG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiC9C;;OAEG;IACH,KAAK,IAAI,IAAI;IASb;;OAEG;WACU,cAAc,CAAC,MAAM,GAAE,MAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxE"}
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import Dexie from 'dexie';
|
|
6
6
|
import { toHex, fromHex } from '@hashtree/core';
|
|
7
|
+
const LAST_ACCESS_FLUSH_DELAY_MS = 50;
|
|
7
8
|
class HashTreeDB extends Dexie {
|
|
8
9
|
blobs;
|
|
10
|
+
accesses;
|
|
9
11
|
constructor(dbName) {
|
|
10
12
|
super(dbName);
|
|
11
13
|
// Version 1: Original schema without lastAccess
|
|
@@ -22,6 +24,19 @@ class HashTreeDB extends Dexie {
|
|
|
22
24
|
blob.lastAccess = now;
|
|
23
25
|
});
|
|
24
26
|
});
|
|
27
|
+
// Version 3: Track access timestamps in a separate table so read hits
|
|
28
|
+
// don't need to rewrite blob payload rows.
|
|
29
|
+
this.version(3).stores({
|
|
30
|
+
blobs: '&hashHex, lastAccess',
|
|
31
|
+
accesses: '&hashHex, lastAccess',
|
|
32
|
+
}).upgrade(async (tx) => {
|
|
33
|
+
const blobs = await tx.table('blobs').toArray();
|
|
34
|
+
const accessesTable = tx.table('accesses');
|
|
35
|
+
await Promise.all(blobs.map((blob) => accessesTable.put({
|
|
36
|
+
hashHex: blob.hashHex,
|
|
37
|
+
lastAccess: blob.lastAccess ?? Date.now(),
|
|
38
|
+
})));
|
|
39
|
+
});
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
/**
|
|
@@ -30,14 +45,63 @@ class HashTreeDB extends Dexie {
|
|
|
30
45
|
*/
|
|
31
46
|
export class DexieStore {
|
|
32
47
|
db;
|
|
48
|
+
pendingLastAccessUpdates = new Map();
|
|
49
|
+
lastAccessFlushPromise = null;
|
|
50
|
+
lastAccessFlushTimer = null;
|
|
33
51
|
constructor(dbName = 'hashtree') {
|
|
34
52
|
this.db = new HashTreeDB(dbName);
|
|
35
53
|
}
|
|
54
|
+
scheduleLastAccessTouch(hashHex) {
|
|
55
|
+
this.pendingLastAccessUpdates.set(hashHex, Date.now());
|
|
56
|
+
if (this.lastAccessFlushTimer !== null) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.lastAccessFlushTimer = setTimeout(() => {
|
|
60
|
+
this.lastAccessFlushTimer = null;
|
|
61
|
+
void this.flushPendingLastAccessUpdates();
|
|
62
|
+
}, LAST_ACCESS_FLUSH_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
async flushPendingLastAccessUpdates() {
|
|
65
|
+
if (this.lastAccessFlushTimer !== null) {
|
|
66
|
+
clearTimeout(this.lastAccessFlushTimer);
|
|
67
|
+
this.lastAccessFlushTimer = null;
|
|
68
|
+
}
|
|
69
|
+
if (this.lastAccessFlushPromise) {
|
|
70
|
+
await this.lastAccessFlushPromise;
|
|
71
|
+
if (this.pendingLastAccessUpdates.size === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (this.pendingLastAccessUpdates.size === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const updates = Array.from(this.pendingLastAccessUpdates, ([hashHex, lastAccess]) => ({ hashHex, lastAccess }));
|
|
79
|
+
this.pendingLastAccessUpdates.clear();
|
|
80
|
+
const pending = this.db.accesses
|
|
81
|
+
.bulkPut(updates)
|
|
82
|
+
.then(() => undefined)
|
|
83
|
+
.catch((e) => {
|
|
84
|
+
console.error('[DexieStore] lastAccess flush error:', e);
|
|
85
|
+
})
|
|
86
|
+
.finally(() => {
|
|
87
|
+
this.lastAccessFlushPromise = null;
|
|
88
|
+
});
|
|
89
|
+
this.lastAccessFlushPromise = pending;
|
|
90
|
+
await pending;
|
|
91
|
+
if (this.pendingLastAccessUpdates.size > 0) {
|
|
92
|
+
await this.flushPendingLastAccessUpdates();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
36
95
|
async put(hash, data) {
|
|
37
96
|
const hashHex = toHex(hash);
|
|
97
|
+
const lastAccess = Date.now();
|
|
38
98
|
try {
|
|
39
|
-
|
|
40
|
-
await this.db.
|
|
99
|
+
await this.flushPendingLastAccessUpdates();
|
|
100
|
+
await this.db.transaction('rw', this.db.blobs, this.db.accesses, async () => {
|
|
101
|
+
// Store directly - IDB will clone the data internally
|
|
102
|
+
await this.db.blobs.put({ hashHex, data, lastAccess });
|
|
103
|
+
await this.db.accesses.put({ hashHex, lastAccess });
|
|
104
|
+
});
|
|
41
105
|
return true;
|
|
42
106
|
}
|
|
43
107
|
catch (e) {
|
|
@@ -53,9 +117,8 @@ export class DexieStore {
|
|
|
53
117
|
const entry = await this.db.blobs.get(hashHex);
|
|
54
118
|
if (!entry)
|
|
55
119
|
return null;
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
this.db.blobs.put({ ...entry, lastAccess: Date.now() }).catch(() => { });
|
|
120
|
+
// Batch LRU touch updates so hot read paths stay read-heavy.
|
|
121
|
+
this.scheduleLastAccessTouch(hashHex);
|
|
59
122
|
// Return directly - IDB returns a fresh copy already
|
|
60
123
|
// Only slice if the view doesn't match the buffer (rare edge case)
|
|
61
124
|
const data = entry.data;
|
|
@@ -85,9 +148,13 @@ export class DexieStore {
|
|
|
85
148
|
async delete(hash) {
|
|
86
149
|
const hashHex = toHex(hash);
|
|
87
150
|
try {
|
|
151
|
+
await this.flushPendingLastAccessUpdates();
|
|
88
152
|
const existed = await this.has(hash);
|
|
89
153
|
if (existed) {
|
|
90
|
-
await this.db.blobs.
|
|
154
|
+
await this.db.transaction('rw', this.db.blobs, this.db.accesses, async () => {
|
|
155
|
+
await this.db.blobs.delete(hashHex);
|
|
156
|
+
await this.db.accesses.delete(hashHex);
|
|
157
|
+
});
|
|
91
158
|
return true;
|
|
92
159
|
}
|
|
93
160
|
return false;
|
|
@@ -116,7 +183,11 @@ export class DexieStore {
|
|
|
116
183
|
*/
|
|
117
184
|
async clear() {
|
|
118
185
|
try {
|
|
119
|
-
await this.
|
|
186
|
+
await this.flushPendingLastAccessUpdates();
|
|
187
|
+
await this.db.transaction('rw', this.db.blobs, this.db.accesses, async () => {
|
|
188
|
+
await this.db.blobs.clear();
|
|
189
|
+
await this.db.accesses.clear();
|
|
190
|
+
});
|
|
120
191
|
}
|
|
121
192
|
catch (e) {
|
|
122
193
|
console.error('[DexieStore] clear error:', e);
|
|
@@ -157,19 +228,24 @@ export class DexieStore {
|
|
|
157
228
|
*/
|
|
158
229
|
async evict(maxBytes) {
|
|
159
230
|
try {
|
|
231
|
+
await this.flushPendingLastAccessUpdates();
|
|
160
232
|
const currentBytes = await this.totalBytes();
|
|
161
233
|
if (currentBytes <= maxBytes)
|
|
162
234
|
return 0;
|
|
163
|
-
// Get entries sorted by lastAccess (oldest first)
|
|
164
|
-
const entries = await this.db.
|
|
235
|
+
// Get entries sorted by lastAccess (oldest first) from the lightweight access table.
|
|
236
|
+
const entries = await this.db.accesses.orderBy('lastAccess').toArray();
|
|
165
237
|
let bytesRemoved = 0;
|
|
166
238
|
let entriesRemoved = 0;
|
|
167
239
|
const targetRemoval = currentBytes - maxBytes;
|
|
168
240
|
for (const entry of entries) {
|
|
169
241
|
if (bytesRemoved >= targetRemoval)
|
|
170
242
|
break;
|
|
171
|
-
await this.db.blobs.
|
|
172
|
-
|
|
243
|
+
const blob = await this.db.blobs.get(entry.hashHex);
|
|
244
|
+
await this.db.transaction('rw', this.db.blobs, this.db.accesses, async () => {
|
|
245
|
+
await this.db.blobs.delete(entry.hashHex);
|
|
246
|
+
await this.db.accesses.delete(entry.hashHex);
|
|
247
|
+
});
|
|
248
|
+
bytesRemoved += blob?.data.byteLength ?? 0;
|
|
173
249
|
entriesRemoved++;
|
|
174
250
|
}
|
|
175
251
|
console.log(`[DexieStore] Evicted ${entriesRemoved} entries (${bytesRemoved} bytes)`);
|
|
@@ -184,6 +260,11 @@ export class DexieStore {
|
|
|
184
260
|
* Close the database connection
|
|
185
261
|
*/
|
|
186
262
|
close() {
|
|
263
|
+
if (this.lastAccessFlushTimer !== null) {
|
|
264
|
+
clearTimeout(this.lastAccessFlushTimer);
|
|
265
|
+
this.lastAccessFlushTimer = null;
|
|
266
|
+
}
|
|
267
|
+
this.pendingLastAccessUpdates.clear();
|
|
187
268
|
this.db.close();
|
|
188
269
|
}
|
|
189
270
|
/**
|
package/dist/index.test.js
CHANGED
|
@@ -212,6 +212,28 @@ describe('DexieStore', () => {
|
|
|
212
212
|
expect(await store.has(hash2)).toBe(false);
|
|
213
213
|
expect(await store.has(hash3)).toBe(true);
|
|
214
214
|
});
|
|
215
|
+
it('should honor a recent get during eviction without waiting for background lastAccess writes', async () => {
|
|
216
|
+
const data1 = new Uint8Array(100);
|
|
217
|
+
const data2 = new Uint8Array(100);
|
|
218
|
+
const data3 = new Uint8Array(100);
|
|
219
|
+
data1[0] = 1;
|
|
220
|
+
data2[0] = 2;
|
|
221
|
+
data3[0] = 3;
|
|
222
|
+
const hash1 = makeHash(data1);
|
|
223
|
+
const hash2 = makeHash(data2);
|
|
224
|
+
const hash3 = makeHash(data3);
|
|
225
|
+
await store.put(hash1, data1);
|
|
226
|
+
await new Promise(r => setTimeout(r, 50));
|
|
227
|
+
await store.put(hash2, data2);
|
|
228
|
+
await new Promise(r => setTimeout(r, 50));
|
|
229
|
+
await store.put(hash3, data3);
|
|
230
|
+
await new Promise(r => setTimeout(r, 50));
|
|
231
|
+
await store.get(hash1);
|
|
232
|
+
await store.evict(200);
|
|
233
|
+
expect(await store.has(hash1)).toBe(true);
|
|
234
|
+
expect(await store.has(hash2)).toBe(false);
|
|
235
|
+
expect(await store.has(hash3)).toBe(true);
|
|
236
|
+
});
|
|
215
237
|
it('should return 0 when store is empty', async () => {
|
|
216
238
|
const evicted = await store.evict(100);
|
|
217
239
|
expect(evicted).toBe(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hashtree/dexie",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Dexie-based IndexedDB store for hashtree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
],
|
|
26
26
|
"author": "Martti Malmi",
|
|
27
27
|
"license": "MIT",
|
|
28
|
+
"repository": "https://git.iris.to/#/npub1xdhnr9mrv47kkrn95k6cwecearydeh8e895990n3acntwvmgk2dsdeeycm/hashtree",
|
|
29
|
+
"homepage": "https://git.iris.to/#/npub1xdhnr9mrv47kkrn95k6cwecearydeh8e895990n3acntwvmgk2dsdeeycm/hashtree/ts/packages/hashtree-dexie",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://git.iris.to/#/npub1xdhnr9mrv47kkrn95k6cwecearydeh8e895990n3acntwvmgk2dsdeeycm/hashtree?tab=issues"
|
|
32
|
+
},
|
|
28
33
|
"dependencies": {
|
|
29
34
|
"dexie": "^4.2.1",
|
|
30
|
-
"@hashtree/core": "0.1.
|
|
35
|
+
"@hashtree/core": "0.1.4"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
38
|
+
"fake-indexeddb": "^6.2.5",
|
|
33
39
|
"typescript": "^5.3.0",
|
|
34
40
|
"vitest": "^2.0.0"
|
|
35
41
|
},
|