@wovin/storage-fs 0.1.36 → 0.2.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.
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{open as S}from"lmdb";import{Logger as c}from"besonders-logger";var{DEBUG:s,VERBOSE:f,ERROR:h}=c.setup(c.INFO),l=class d{root;applogs;threadIndex;blocks;static async init(t){s(`[StorageFs.init] ENTER - path=${t}`),s("[StorageFs.init] Opening LMDB root database");let o=S({path:t,compression:!0});s("[StorageFs.init] LMDB root database opened"),s("[StorageFs.init] Opening applogs DB");let r=o.openDB({name:"applogs"});s("[StorageFs.init] Applogs DB opened"),s("[StorageFs.init] Opening threadIndex DB");let e=o.openDB({name:"thread_index",dupSort:!0});s("[StorageFs.init] ThreadIndex DB opened"),s("[StorageFs.init] Opening blocks DB");let n=o.openDB({name:"blocks"});s("[StorageFs.init] Blocks DB opened"),s("[StorageFs.init] Creating StorageFs instance");let p=new d(o,r,e,n);return s("[StorageFs.init] EXIT - StorageFs instance created successfully"),p}constructor(t,o,r,e){this.root=t,this.applogs=o,this.threadIndex=r,this.blocks=e}async storeApplogs(t,o){let r=Date.now();s("[StorageFs.storeApplogs] ===== ENTER ====="),s(`[StorageFs.storeApplogs] threadId: ${t}`),s(`[StorageFs.storeApplogs] applogs.length: ${o.length}`);let e=0;s("[StorageFs.storeApplogs] About to start transaction");let n=Date.now();try{this.root.transactionSync(()=>{for(let g of o){let i=g.cid;if(this.applogs.doesExist(i)){this.threadIndex.doesExist(t,i)||this.threadIndex.put(t,i);continue}this.applogs.put(i,JSON.stringify(g)),this.threadIndex.put(t,i),e++}});let a=Date.now()-n;s(`[StorageFs.storeApplogs] Transaction completed in ${a}ms`)}catch(a){let g=Date.now()-n;throw h(`[StorageFs.storeApplogs] Transaction error after ${g}ms:`,a),a}let p=Date.now()-r;return s(`[StorageFs.storeApplogs] ===== SUCCESS in ${p}ms, stored ${e} applogs`),e}getApplogs(t){let o=[],r=this.threadIndex.getValues(t);for(let e of r){let n=this.applogs.get(e);n&&o.push(JSON.parse(n))}return o}getApplogByCid(t){let o=typeof t=="string"?t:t.toString(),r=this.applogs.get(o);return r?JSON.parse(r):null}hasApplog(t){let o=typeof t=="string"?t:t.toString();return this.applogs.doesExist(o)}async get(t){let o=this.blocks.get(t.toString());if(!o)throw new Error(`Block not found: ${t}`);return o}async put(t,o){await this.blocks.put(t.toString(),o)}async has(t){return this.blocks.doesExist(t.toString())}listThreads(){let t=new Set;for(let{key:o}of this.threadIndex.getRange({}))t.add(o);return Array.from(t)}async deleteThread(t){this.root.transactionSync(()=>{let o=Array.from(this.threadIndex.getValues(t));for(let r of o)this.threadIndex.remove(t,r)}),s(`[StorageFs] Deleted thread "${t}"`)}getThreadApplogCount(t){return this.threadIndex.getValuesCount(t)}isApplogInThread(t,o){let r=typeof o=="string"?o:o.toString();return this.threadIndex.doesExist(t,r)}close(){this.root.close()}};export{l as LmdbConnector,l as StorageFs};
|
|
2
|
-
//# sourceMappingURL=index.
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wovin/storage-fs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./dist/index.
|
|
6
|
-
"module": "./dist/index.
|
|
7
|
-
"browser": "./dist/index.
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"browser": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"import": "./dist/index.
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"./dist/"
|
|
16
|
+
"./dist/",
|
|
17
|
+
"./src/"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"besonders-logger": "1.0.1",
|
|
27
28
|
"lmdb": "^3.0.0",
|
|
28
29
|
"multiformats": "^13.0.1",
|
|
29
|
-
"@wovin/core": "^0.
|
|
30
|
+
"@wovin/core": "^0.2.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"concurrently": "^8.2.2",
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { open, RootDatabase, Database } from 'lmdb'
|
|
2
|
+
import type { Applog, CidString } from '@wovin/core'
|
|
3
|
+
import type { BlockStore } from '@wovin/core/blockstore'
|
|
4
|
+
import { Logger } from 'besonders-logger'
|
|
5
|
+
import { CID } from 'multiformats/cid'
|
|
6
|
+
|
|
7
|
+
const { DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* File-system based storage using LMDB.
|
|
11
|
+
* Implements BlockStore for use with wovin block caching and snapshot chain walking.
|
|
12
|
+
*/
|
|
13
|
+
export class StorageFs implements BlockStore {
|
|
14
|
+
private root: RootDatabase
|
|
15
|
+
private applogs: Database<string, string> // cidString → Applog JSON
|
|
16
|
+
private threadIndex: Database<string, string> // threadId → cidString (dupSort)
|
|
17
|
+
private blocks: Database<Uint8Array, string> // cidString → bytes
|
|
18
|
+
|
|
19
|
+
static async init(path: string): Promise<StorageFs> {
|
|
20
|
+
DEBUG(`[StorageFs.init] ENTER - path=${path}`)
|
|
21
|
+
|
|
22
|
+
DEBUG(`[StorageFs.init] Opening LMDB root database`)
|
|
23
|
+
const root = open({
|
|
24
|
+
path,
|
|
25
|
+
compression: true,
|
|
26
|
+
})
|
|
27
|
+
DEBUG(`[StorageFs.init] LMDB root database opened`)
|
|
28
|
+
|
|
29
|
+
DEBUG(`[StorageFs.init] Opening applogs DB`)
|
|
30
|
+
const applogs = root.openDB<string, string>({
|
|
31
|
+
name: 'applogs',
|
|
32
|
+
})
|
|
33
|
+
DEBUG(`[StorageFs.init] Applogs DB opened`)
|
|
34
|
+
|
|
35
|
+
DEBUG(`[StorageFs.init] Opening threadIndex DB`)
|
|
36
|
+
const threadIndex = root.openDB<string, string>({
|
|
37
|
+
name: 'thread_index',
|
|
38
|
+
dupSort: true,
|
|
39
|
+
})
|
|
40
|
+
DEBUG(`[StorageFs.init] ThreadIndex DB opened`)
|
|
41
|
+
|
|
42
|
+
DEBUG(`[StorageFs.init] Opening blocks DB`)
|
|
43
|
+
const blocks = root.openDB<Uint8Array, string>({
|
|
44
|
+
name: 'blocks',
|
|
45
|
+
})
|
|
46
|
+
DEBUG(`[StorageFs.init] Blocks DB opened`)
|
|
47
|
+
|
|
48
|
+
DEBUG(`[StorageFs.init] Creating StorageFs instance`)
|
|
49
|
+
const instance = new StorageFs(root, applogs, threadIndex, blocks)
|
|
50
|
+
DEBUG(`[StorageFs.init] EXIT - StorageFs instance created successfully`)
|
|
51
|
+
return instance
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private constructor(
|
|
55
|
+
root: RootDatabase,
|
|
56
|
+
applogs: Database<string, string>,
|
|
57
|
+
threadIndex: Database<string, string>,
|
|
58
|
+
blocks: Database<Uint8Array, string>,
|
|
59
|
+
) {
|
|
60
|
+
this.root = root
|
|
61
|
+
this.applogs = applogs
|
|
62
|
+
this.threadIndex = threadIndex
|
|
63
|
+
this.blocks = blocks
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// === Applogs ===
|
|
67
|
+
|
|
68
|
+
async storeApplogs(threadId: string, applogs: Applog[]): Promise<number> {
|
|
69
|
+
const fnStart = Date.now()
|
|
70
|
+
DEBUG(`[StorageFs.storeApplogs] ===== ENTER =====`)
|
|
71
|
+
DEBUG(`[StorageFs.storeApplogs] threadId: ${threadId}`)
|
|
72
|
+
DEBUG(`[StorageFs.storeApplogs] applogs.length: ${applogs.length}`)
|
|
73
|
+
let stored = 0
|
|
74
|
+
|
|
75
|
+
DEBUG(`[StorageFs.storeApplogs] About to start transaction`)
|
|
76
|
+
const txStart = Date.now()
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// CRITICAL FIX: Use transactionSync instead of transaction
|
|
80
|
+
// Async transactions (transaction method) are NOT supported in Deno's NAPI
|
|
81
|
+
// Reference: https://github.com/kriszyp/lmdb-js#usage
|
|
82
|
+
// "Deno and Bun's support for NAPI is not very stable yet, and currently
|
|
83
|
+
// asynchronous transactions (transaction method) are not supported"
|
|
84
|
+
|
|
85
|
+
// NOTE: No logging inside transaction - LMDB transactions must be fast and synchronous
|
|
86
|
+
this.root.transactionSync(() => {
|
|
87
|
+
for (const applog of applogs) {
|
|
88
|
+
const cidStr = applog.cid
|
|
89
|
+
|
|
90
|
+
// Check if already stored (dedup by CID)
|
|
91
|
+
if (this.applogs.doesExist(cidStr)) {
|
|
92
|
+
// Applog exists, but still add to thread index if not already there
|
|
93
|
+
if (!this.threadIndex.doesExist(threadId, cidStr)) {
|
|
94
|
+
this.threadIndex.put(threadId, cidStr)
|
|
95
|
+
}
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Store applog
|
|
100
|
+
this.applogs.put(cidStr, JSON.stringify(applog))
|
|
101
|
+
|
|
102
|
+
// Add to thread index
|
|
103
|
+
this.threadIndex.put(threadId, cidStr)
|
|
104
|
+
|
|
105
|
+
stored++
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const txDuration = Date.now() - txStart
|
|
110
|
+
DEBUG(`[StorageFs.storeApplogs] Transaction completed in ${txDuration}ms`)
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const txDuration = Date.now() - txStart
|
|
113
|
+
ERROR(`[StorageFs.storeApplogs] Transaction error after ${txDuration}ms:`, err)
|
|
114
|
+
throw err
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const fnDuration = Date.now() - fnStart
|
|
118
|
+
DEBUG(`[StorageFs.storeApplogs] ===== SUCCESS in ${fnDuration}ms, stored ${stored} applogs`)
|
|
119
|
+
return stored
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getApplogs(threadId: string): Applog[] {
|
|
123
|
+
const applogs: Applog[] = []
|
|
124
|
+
const cidStrings = this.threadIndex.getValues(threadId)
|
|
125
|
+
|
|
126
|
+
for (const cidStr of cidStrings) {
|
|
127
|
+
const json = this.applogs.get(cidStr)
|
|
128
|
+
if (json) {
|
|
129
|
+
applogs.push(JSON.parse(json))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return applogs
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getApplogByCid(cid: CID | CidString): Applog | null {
|
|
137
|
+
const cidStr = typeof cid === 'string' ? cid : cid.toString()
|
|
138
|
+
const json = this.applogs.get(cidStr)
|
|
139
|
+
if (!json) return null
|
|
140
|
+
return JSON.parse(json)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hasApplog(cid: CID | CidString): boolean {
|
|
144
|
+
const cidStr = typeof cid === 'string' ? cid : cid.toString()
|
|
145
|
+
return this.applogs.doesExist(cidStr)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// === Blocks (BlockStore interface) ===
|
|
149
|
+
|
|
150
|
+
async get(cid: CID): Promise<Uint8Array> {
|
|
151
|
+
const bytes = this.blocks.get(cid.toString())
|
|
152
|
+
if (!bytes) throw new Error(`Block not found: ${cid}`)
|
|
153
|
+
return bytes
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async put(cid: CID, bytes: Uint8Array): Promise<void> {
|
|
157
|
+
await this.blocks.put(cid.toString(), bytes)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async has(cid: CID): Promise<boolean> {
|
|
161
|
+
return this.blocks.doesExist(cid.toString())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// === Threads ===
|
|
165
|
+
|
|
166
|
+
listThreads(): string[] {
|
|
167
|
+
const threads = new Set<string>()
|
|
168
|
+
// Iterate over all keys in thread_index
|
|
169
|
+
for (const { key } of this.threadIndex.getRange({})) {
|
|
170
|
+
threads.add(key)
|
|
171
|
+
}
|
|
172
|
+
return Array.from(threads)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async deleteThread(threadId: string): Promise<void> {
|
|
176
|
+
// Use transactionSync for Deno compatibility (async transactions not supported in Deno NAPI)
|
|
177
|
+
this.root.transactionSync(() => {
|
|
178
|
+
// Get all CIDs in this thread
|
|
179
|
+
const cidStrings = Array.from(this.threadIndex.getValues(threadId))
|
|
180
|
+
|
|
181
|
+
// Remove all entries for this thread from index
|
|
182
|
+
for (const cidStr of cidStrings) {
|
|
183
|
+
this.threadIndex.remove(threadId, cidStr)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Note: We don't delete the applogs themselves - they may be in other threads
|
|
187
|
+
// Orphan cleanup could be a separate maintenance operation
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
DEBUG(`[StorageFs] Deleted thread "${threadId}"`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// === Utilities ===
|
|
194
|
+
|
|
195
|
+
getThreadApplogCount(threadId: string): number {
|
|
196
|
+
return this.threadIndex.getValuesCount(threadId)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
isApplogInThread(threadId: string, cid: CID | CidString): boolean {
|
|
200
|
+
const cidStr = typeof cid === 'string' ? cid : cid.toString()
|
|
201
|
+
return this.threadIndex.doesExist(threadId, cidStr)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
close(): void {
|
|
205
|
+
this.root.close()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Re-export for backwards compatibility during transition
|
|
210
|
+
export { StorageFs as LmdbConnector }
|
|
File without changes
|