@livestore/sqlite-wasm 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db
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/.tsbuildinfo +1 -0
- package/dist/FacadeVFS.d.ts +243 -0
- package/dist/FacadeVFS.d.ts.map +1 -0
- package/dist/FacadeVFS.js +474 -0
- package/dist/FacadeVFS.js.map +1 -0
- package/dist/browser/mod.d.ts +44 -0
- package/dist/browser/mod.d.ts.map +1 -0
- package/dist/browser/mod.js +51 -0
- package/dist/browser/mod.js.map +1 -0
- package/dist/browser/opfs/AccessHandlePoolVFS.d.ts +47 -0
- package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -0
- package/dist/browser/opfs/AccessHandlePoolVFS.js +355 -0
- package/dist/browser/opfs/AccessHandlePoolVFS.js.map +1 -0
- package/dist/browser/opfs/index.d.ts +12 -0
- package/dist/browser/opfs/index.d.ts.map +1 -0
- package/dist/browser/opfs/index.js +19 -0
- package/dist/browser/opfs/index.js.map +1 -0
- package/dist/browser/opfs/opfs-sah-pool.d.ts +3 -0
- package/dist/browser/opfs/opfs-sah-pool.d.ts.map +1 -0
- package/dist/browser/opfs/opfs-sah-pool.js +55 -0
- package/dist/browser/opfs/opfs-sah-pool.js.map +1 -0
- package/dist/in-memory-vfs.d.ts +7 -0
- package/dist/in-memory-vfs.d.ts.map +1 -0
- package/dist/in-memory-vfs.js +15 -0
- package/dist/in-memory-vfs.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index_.d.ts +3 -0
- package/dist/index_.d.ts.map +1 -0
- package/dist/index_.js +3 -0
- package/dist/index_.js.map +1 -0
- package/dist/load-wasm/mod.browser.d.ts +2 -0
- package/dist/load-wasm/mod.browser.d.ts.map +1 -0
- package/dist/load-wasm/mod.browser.js +12 -0
- package/dist/load-wasm/mod.browser.js.map +1 -0
- package/dist/load-wasm/mod.node.d.ts +2 -0
- package/dist/load-wasm/mod.node.d.ts.map +1 -0
- package/dist/load-wasm/mod.node.js +13 -0
- package/dist/load-wasm/mod.node.js.map +1 -0
- package/dist/make-sqlite-db.d.ts +11 -0
- package/dist/make-sqlite-db.d.ts.map +1 -0
- package/dist/make-sqlite-db.js +181 -0
- package/dist/make-sqlite-db.js.map +1 -0
- package/dist/node/NodeFS.d.ts +20 -0
- package/dist/node/NodeFS.d.ts.map +1 -0
- package/dist/node/NodeFS.js +174 -0
- package/dist/node/NodeFS.js.map +1 -0
- package/dist/node/mod.d.ts +41 -0
- package/dist/node/mod.d.ts.map +1 -0
- package/dist/node/mod.js +61 -0
- package/dist/node/mod.js.map +1 -0
- package/package.json +38 -0
- package/src/FacadeVFS.ts +510 -0
- package/src/ambient.d.ts +18 -0
- package/src/browser/mod.ts +109 -0
- package/src/browser/opfs/AccessHandlePoolVFS.ts +404 -0
- package/src/browser/opfs/index.ts +35 -0
- package/src/browser/opfs/opfs-sah-pool.ts +68 -0
- package/src/in-memory-vfs.ts +20 -0
- package/src/index.ts +1 -0
- package/src/index_.ts +2 -0
- package/src/load-wasm/mod.browser.ts +12 -0
- package/src/load-wasm/mod.node.ts +13 -0
- package/src/make-sqlite-db.ts +220 -0
- package/src/node/NodeFS.ts +190 -0
- package/src/node/mod.ts +132 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PersistenceInfo,
|
|
3
|
+
PreparedBindValues,
|
|
4
|
+
PreparedStatement,
|
|
5
|
+
SqliteDb,
|
|
6
|
+
SqliteDbChangeset,
|
|
7
|
+
} from '@livestore/common'
|
|
8
|
+
import { SqliteError } from '@livestore/common'
|
|
9
|
+
import * as SqliteConstants from '@livestore/wa-sqlite/src/sqlite-constants.js'
|
|
10
|
+
|
|
11
|
+
import { makeInMemoryDb } from './in-memory-vfs.js'
|
|
12
|
+
|
|
13
|
+
export const makeSqliteDb = <
|
|
14
|
+
TMetadata extends {
|
|
15
|
+
dbPointer: number
|
|
16
|
+
persistenceInfo: PersistenceInfo
|
|
17
|
+
deleteDb: () => void
|
|
18
|
+
configureDb: (db: SqliteDb<TMetadata>) => void
|
|
19
|
+
},
|
|
20
|
+
>({
|
|
21
|
+
sqlite3,
|
|
22
|
+
metadata,
|
|
23
|
+
}: {
|
|
24
|
+
sqlite3: SQLiteAPI
|
|
25
|
+
metadata: TMetadata
|
|
26
|
+
}): SqliteDb<TMetadata> => {
|
|
27
|
+
const preparedStmts: PreparedStatement[] = []
|
|
28
|
+
const { dbPointer } = metadata
|
|
29
|
+
|
|
30
|
+
let isClosed = false
|
|
31
|
+
|
|
32
|
+
const sqliteDb: SqliteDb<TMetadata> = {
|
|
33
|
+
_tag: 'SqliteDb',
|
|
34
|
+
metadata,
|
|
35
|
+
prepare: (queryStr) => {
|
|
36
|
+
try {
|
|
37
|
+
const stmts = sqlite3.statements(dbPointer, queryStr.trim(), { unscoped: true })
|
|
38
|
+
|
|
39
|
+
let isFinalized = false
|
|
40
|
+
|
|
41
|
+
const preparedStmt = {
|
|
42
|
+
execute: (bindValues, options) => {
|
|
43
|
+
for (const stmt of stmts) {
|
|
44
|
+
if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
|
|
45
|
+
sqlite3.bind_collection(stmt, bindValues as any)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
sqlite3.step(stmt)
|
|
50
|
+
} finally {
|
|
51
|
+
if (options?.onRowsChanged) {
|
|
52
|
+
options.onRowsChanged(sqlite3.changes(dbPointer))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
sqlite3.reset(stmt) // Reset is needed for next execution
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
select: <T>(bindValues: PreparedBindValues) => {
|
|
60
|
+
if (stmts.length !== 1) {
|
|
61
|
+
throw new SqliteError({
|
|
62
|
+
query: { bindValues, sql: queryStr },
|
|
63
|
+
code: -1,
|
|
64
|
+
cause: 'Expected only one statement when using `select`',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const stmt = stmts[0]!
|
|
69
|
+
|
|
70
|
+
if (bindValues !== undefined && Object.keys(bindValues).length > 0) {
|
|
71
|
+
sqlite3.bind_collection(stmt, bindValues as any)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const results: T[] = []
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// NOTE `column_names` only works for `SELECT` statements, ignoring other statements for now
|
|
78
|
+
let columns = undefined
|
|
79
|
+
try {
|
|
80
|
+
columns = sqlite3.column_names(stmt)
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
82
|
+
} catch (_e) {}
|
|
83
|
+
|
|
84
|
+
while (sqlite3.step(stmt) === SqliteConstants.SQLITE_ROW) {
|
|
85
|
+
if (columns !== undefined) {
|
|
86
|
+
const obj: { [key: string]: any } = {}
|
|
87
|
+
for (let i = 0; i < columns.length; i++) {
|
|
88
|
+
obj[columns[i]!] = sqlite3.column(stmt, i)
|
|
89
|
+
}
|
|
90
|
+
results.push(obj as unknown as T)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw new SqliteError({
|
|
95
|
+
query: { bindValues, sql: queryStr },
|
|
96
|
+
code: (e as any).code,
|
|
97
|
+
cause: e,
|
|
98
|
+
})
|
|
99
|
+
} finally {
|
|
100
|
+
// reset the cached statement so we can use it again in the future
|
|
101
|
+
sqlite3.reset(stmt)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results
|
|
105
|
+
},
|
|
106
|
+
finalize: () => {
|
|
107
|
+
// Avoid double finalization which leads to a crash
|
|
108
|
+
if (isFinalized) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isFinalized = true
|
|
113
|
+
|
|
114
|
+
for (const stmt of stmts) {
|
|
115
|
+
sqlite3.finalize(stmt)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
sql: queryStr,
|
|
119
|
+
} satisfies PreparedStatement
|
|
120
|
+
|
|
121
|
+
preparedStmts.push(preparedStmt)
|
|
122
|
+
|
|
123
|
+
return preparedStmt
|
|
124
|
+
} catch (e) {
|
|
125
|
+
throw new SqliteError({
|
|
126
|
+
query: { sql: queryStr, bindValues: {} },
|
|
127
|
+
code: (e as any).code,
|
|
128
|
+
cause: e,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
export: () => sqlite3.serialize(dbPointer, 'main'),
|
|
133
|
+
execute: (queryStr, bindValues, options) => {
|
|
134
|
+
const stmt = sqliteDb.prepare(queryStr)
|
|
135
|
+
stmt.execute(bindValues, options)
|
|
136
|
+
stmt.finalize()
|
|
137
|
+
},
|
|
138
|
+
select: (queryStr, bindValues) => {
|
|
139
|
+
const stmt = sqliteDb.prepare(queryStr)
|
|
140
|
+
const results = stmt.select(bindValues)
|
|
141
|
+
stmt.finalize()
|
|
142
|
+
return results as ReadonlyArray<any>
|
|
143
|
+
},
|
|
144
|
+
destroy: () => {
|
|
145
|
+
sqliteDb.close()
|
|
146
|
+
|
|
147
|
+
metadata.deleteDb()
|
|
148
|
+
// if (metadata._tag === 'opfs') {
|
|
149
|
+
// metadata.vfs.resetAccessHandle(metadata.fileName)
|
|
150
|
+
// }
|
|
151
|
+
},
|
|
152
|
+
close: () => {
|
|
153
|
+
if (isClosed) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const stmt of preparedStmts) {
|
|
158
|
+
stmt.finalize()
|
|
159
|
+
}
|
|
160
|
+
sqlite3.close(dbPointer)
|
|
161
|
+
isClosed = true
|
|
162
|
+
},
|
|
163
|
+
import: (source) => {
|
|
164
|
+
// https://www.sqlite.org/c3ref/c_deserialize_freeonclose.html
|
|
165
|
+
// #define SQLITE_DESERIALIZE_FREEONCLOSE 1 /* Call sqlite3_free() on close */
|
|
166
|
+
// #define SQLITE_DESERIALIZE_RESIZEABLE 2 /* Resize using sqlite3_realloc64() */
|
|
167
|
+
// #define SQLITE_DESERIALIZE_READONLY 4 /* Database is read-only */
|
|
168
|
+
const FREE_ON_CLOSE = 1
|
|
169
|
+
const RESIZEABLE = 2
|
|
170
|
+
|
|
171
|
+
// NOTE in case we'll have a future use-case where we need a read-only database, we can reuse this code below
|
|
172
|
+
// if (readOnly === true) {
|
|
173
|
+
// sqlite3.deserialize(db, 'main', bytes, bytes.length, bytes.length, FREE_ON_CLOSE | RESIZEABLE)
|
|
174
|
+
// } else {
|
|
175
|
+
if (source instanceof Uint8Array) {
|
|
176
|
+
const tmpDb = makeInMemoryDb(sqlite3)
|
|
177
|
+
// TODO find a way to do this more efficiently with sqlite to avoid either of the deserialize + backup call
|
|
178
|
+
// Maybe this can be done via the VFS API
|
|
179
|
+
sqlite3.deserialize(tmpDb.dbPointer, 'main', source, source.length, source.length, FREE_ON_CLOSE | RESIZEABLE)
|
|
180
|
+
sqlite3.backup(dbPointer, 'main', tmpDb.dbPointer, 'main')
|
|
181
|
+
sqlite3.close(tmpDb.dbPointer)
|
|
182
|
+
} else {
|
|
183
|
+
sqlite3.backup(dbPointer, 'main', source.metadata.dbPointer, 'main')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
metadata.configureDb(sqliteDb)
|
|
187
|
+
},
|
|
188
|
+
session: () => {
|
|
189
|
+
const sessionPointer = sqlite3.session_create(dbPointer, 'main')
|
|
190
|
+
sqlite3.session_attach(sessionPointer, null)
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
changeset: () => {
|
|
194
|
+
const res = sqlite3.session_changeset(sessionPointer)
|
|
195
|
+
return res.changeset ?? undefined
|
|
196
|
+
},
|
|
197
|
+
finish: () => {
|
|
198
|
+
sqlite3.session_delete(sessionPointer)
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
makeChangeset: (data) => {
|
|
203
|
+
const changeset = {
|
|
204
|
+
invert: () => {
|
|
205
|
+
const inverted = sqlite3.changeset_invert(data)
|
|
206
|
+
return sqliteDb.makeChangeset(inverted)
|
|
207
|
+
},
|
|
208
|
+
apply: () => {
|
|
209
|
+
sqlite3.changeset_apply(dbPointer, data)
|
|
210
|
+
},
|
|
211
|
+
} satisfies SqliteDbChangeset
|
|
212
|
+
|
|
213
|
+
return changeset
|
|
214
|
+
},
|
|
215
|
+
} satisfies SqliteDb<TMetadata>
|
|
216
|
+
|
|
217
|
+
metadata.configureDb(sqliteDb)
|
|
218
|
+
|
|
219
|
+
return sqliteDb
|
|
220
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
4
|
+
import * as fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
import type * as WaSqlite from '@livestore/wa-sqlite'
|
|
8
|
+
import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
|
|
9
|
+
|
|
10
|
+
import { FacadeVFS } from '../FacadeVFS.js'
|
|
11
|
+
|
|
12
|
+
interface NodeFsFile {
|
|
13
|
+
pathname: string
|
|
14
|
+
flags: number
|
|
15
|
+
fileHandle: number | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class NodeFS extends FacadeVFS {
|
|
19
|
+
private mapIdToFile = new Map<number, NodeFsFile>()
|
|
20
|
+
private lastError: Error | null = null
|
|
21
|
+
private readonly directory: string
|
|
22
|
+
constructor(name: string, sqlite3: WaSqlite.SQLiteAPI, directory: string) {
|
|
23
|
+
super(name, sqlite3)
|
|
24
|
+
|
|
25
|
+
this.directory = directory
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getFilename(fileId: number): string {
|
|
29
|
+
const pathname = this.mapIdToFile.get(fileId)?.pathname
|
|
30
|
+
return `NodeFS:${pathname}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
jOpen(zName: string | null, fileId: number, flags: number, pOutFlags: DataView): number {
|
|
34
|
+
try {
|
|
35
|
+
const pathname = zName ? path.resolve(this.directory, zName) : Math.random().toString(36).slice(2)
|
|
36
|
+
const file: NodeFsFile = { pathname, flags, fileHandle: null }
|
|
37
|
+
this.mapIdToFile.set(fileId, file)
|
|
38
|
+
|
|
39
|
+
const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
|
|
40
|
+
const readwrite = !!(flags & VFS.SQLITE_OPEN_READWRITE)
|
|
41
|
+
|
|
42
|
+
// Convert SQLite flags to Node.js flags
|
|
43
|
+
let fsFlags = 'r'
|
|
44
|
+
if (create && readwrite) {
|
|
45
|
+
// Check if file exists first
|
|
46
|
+
const exists = fs.existsSync(pathname)
|
|
47
|
+
fsFlags = exists ? 'r+' : 'w+' // Use r+ for existing files, w+ only for new files
|
|
48
|
+
} else if (readwrite) {
|
|
49
|
+
fsFlags = 'r+' // Open file for reading and writing
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
file.fileHandle = fs.openSync(pathname, fsFlags)
|
|
54
|
+
pOutFlags.setInt32(0, flags, true)
|
|
55
|
+
return VFS.SQLITE_OK
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
if (err.code === 'ENOENT' && !create) {
|
|
58
|
+
return VFS.SQLITE_CANTOPEN
|
|
59
|
+
}
|
|
60
|
+
throw err
|
|
61
|
+
}
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
this.lastError = e
|
|
64
|
+
return VFS.SQLITE_CANTOPEN
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
jRead(fileId: number, pData: Uint8Array, iOffset: number): number {
|
|
69
|
+
try {
|
|
70
|
+
const file = this.mapIdToFile.get(fileId)
|
|
71
|
+
if (!file?.fileHandle) return VFS.SQLITE_IOERR_READ
|
|
72
|
+
|
|
73
|
+
// const view = new DataView(pData.buffer, pData.byteOffset, pData.length)
|
|
74
|
+
// const bytesRead = fs.readSync(file.fileHandle, view, 0, pData.length, iOffset)
|
|
75
|
+
const bytesRead = fs.readSync(file.fileHandle, pData.subarray(), { position: iOffset })
|
|
76
|
+
|
|
77
|
+
if (bytesRead < pData.length) {
|
|
78
|
+
pData.fill(0, bytesRead)
|
|
79
|
+
return VFS.SQLITE_IOERR_SHORT_READ
|
|
80
|
+
}
|
|
81
|
+
return VFS.SQLITE_OK
|
|
82
|
+
} catch (e: any) {
|
|
83
|
+
this.lastError = e
|
|
84
|
+
return VFS.SQLITE_IOERR_READ
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
jWrite(fileId: number, pData: Uint8Array, iOffset: number): number {
|
|
89
|
+
try {
|
|
90
|
+
const file = this.mapIdToFile.get(fileId)
|
|
91
|
+
if (!file?.fileHandle) return VFS.SQLITE_IOERR_WRITE
|
|
92
|
+
|
|
93
|
+
// const view = new DataView(pData.buffer, pData.byteOffset, pData.length)
|
|
94
|
+
// fs.writeSync(file.fileHandle, view, 0, pData.length, iOffset)
|
|
95
|
+
fs.writeSync(file.fileHandle, Buffer.from(pData.subarray()), 0, pData.length, iOffset)
|
|
96
|
+
return VFS.SQLITE_OK
|
|
97
|
+
} catch (e: any) {
|
|
98
|
+
this.lastError = e
|
|
99
|
+
return VFS.SQLITE_IOERR_WRITE
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
jClose(fileId: number): number {
|
|
104
|
+
try {
|
|
105
|
+
const file = this.mapIdToFile.get(fileId)
|
|
106
|
+
if (!file) return VFS.SQLITE_OK
|
|
107
|
+
|
|
108
|
+
this.mapIdToFile.delete(fileId)
|
|
109
|
+
if (file.fileHandle !== null) {
|
|
110
|
+
fs.closeSync(file.fileHandle)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
114
|
+
fs.unlinkSync(file.pathname)
|
|
115
|
+
}
|
|
116
|
+
return VFS.SQLITE_OK
|
|
117
|
+
} catch (e: any) {
|
|
118
|
+
this.lastError = e
|
|
119
|
+
return VFS.SQLITE_IOERR_CLOSE
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
jFileSize(fileId: number, pSize64: DataView): number {
|
|
124
|
+
try {
|
|
125
|
+
const file = this.mapIdToFile.get(fileId)
|
|
126
|
+
if (!file?.fileHandle) return VFS.SQLITE_IOERR_FSTAT
|
|
127
|
+
|
|
128
|
+
const stats = fs.fstatSync(file.fileHandle)
|
|
129
|
+
pSize64.setBigInt64(0, BigInt(stats.size), true)
|
|
130
|
+
return VFS.SQLITE_OK
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
this.lastError = e
|
|
133
|
+
return VFS.SQLITE_IOERR_FSTAT
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
jTruncate(fileId: number, iSize: number): number {
|
|
138
|
+
try {
|
|
139
|
+
const file = this.mapIdToFile.get(fileId)
|
|
140
|
+
if (!file?.fileHandle) return VFS.SQLITE_IOERR_TRUNCATE
|
|
141
|
+
|
|
142
|
+
fs.ftruncateSync(file.fileHandle, iSize)
|
|
143
|
+
return VFS.SQLITE_OK
|
|
144
|
+
} catch (e: any) {
|
|
145
|
+
this.lastError = e
|
|
146
|
+
return VFS.SQLITE_IOERR_TRUNCATE
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
jSync(fileId: number, _flags: number): number {
|
|
151
|
+
try {
|
|
152
|
+
const file = this.mapIdToFile.get(fileId)
|
|
153
|
+
if (!file?.fileHandle) return VFS.SQLITE_OK
|
|
154
|
+
|
|
155
|
+
// TODO do this out of band (for now we disable it to speed up the node vfs)
|
|
156
|
+
// fs.fsyncSync(file.fileHandle)
|
|
157
|
+
return VFS.SQLITE_OK
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
this.lastError = e
|
|
160
|
+
return VFS.SQLITE_IOERR_FSYNC
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
jDelete(zName: string, _syncDir: number): number {
|
|
165
|
+
try {
|
|
166
|
+
const pathname = path.resolve(this.directory, zName)
|
|
167
|
+
fs.unlinkSync(pathname)
|
|
168
|
+
return VFS.SQLITE_OK
|
|
169
|
+
} catch (e: any) {
|
|
170
|
+
this.lastError = e
|
|
171
|
+
return VFS.SQLITE_IOERR_DELETE
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
jAccess(zName: string, _flags: number, pResOut: DataView): number {
|
|
176
|
+
try {
|
|
177
|
+
const pathname = path.resolve(this.directory, zName)
|
|
178
|
+
const exists = fs.existsSync(pathname)
|
|
179
|
+
pResOut.setInt32(0, exists ? 1 : 0, true)
|
|
180
|
+
return VFS.SQLITE_OK
|
|
181
|
+
} catch (e: any) {
|
|
182
|
+
this.lastError = e
|
|
183
|
+
return VFS.SQLITE_IOERR_ACCESS
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
deleteDb(fileName: string) {
|
|
188
|
+
fs.unlinkSync(path.join(this.directory, fileName))
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/node/mod.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { type MakeSqliteDb, type PersistenceInfo, type SqliteDb, UnexpectedError } from '@livestore/common'
|
|
4
|
+
import { Effect, FileSystem } from '@livestore/utils/effect'
|
|
5
|
+
import type * as WaSqlite from '@livestore/wa-sqlite'
|
|
6
|
+
import type { MemoryVFS } from '@livestore/wa-sqlite/src/examples/MemoryVFS.js'
|
|
7
|
+
|
|
8
|
+
import { makeInMemoryDb } from '../in-memory-vfs.js'
|
|
9
|
+
import { makeSqliteDb } from '../make-sqlite-db.js'
|
|
10
|
+
import { NodeFS } from './NodeFS.js'
|
|
11
|
+
|
|
12
|
+
export type NodeDatabaseMetadataInMemory = {
|
|
13
|
+
_tag: 'in-memory'
|
|
14
|
+
vfs: MemoryVFS
|
|
15
|
+
dbPointer: number
|
|
16
|
+
persistenceInfo: PersistenceInfo
|
|
17
|
+
deleteDb: () => void
|
|
18
|
+
configureDb: (db: SqliteDb) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type NodeDatabaseMetadataFs = {
|
|
22
|
+
_tag: 'fs'
|
|
23
|
+
vfs: NodeFS
|
|
24
|
+
dbPointer: number
|
|
25
|
+
persistenceInfo: PersistenceInfo<{ directory: string }>
|
|
26
|
+
deleteDb: () => void
|
|
27
|
+
configureDb: (db: SqliteDb) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type NodeDatabaseMetadata = NodeDatabaseMetadataInMemory | NodeDatabaseMetadataFs
|
|
31
|
+
|
|
32
|
+
export type NodeDatabaseInputInMemory = {
|
|
33
|
+
_tag: 'in-memory'
|
|
34
|
+
configureDb?: (db: SqliteDb) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type NodeDatabaseInputFs = {
|
|
38
|
+
_tag: 'fs'
|
|
39
|
+
directory: string
|
|
40
|
+
fileName: string
|
|
41
|
+
configureDb?: (db: SqliteDb) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type NodeDatabaseInput = NodeDatabaseInputInMemory | NodeDatabaseInputFs
|
|
45
|
+
|
|
46
|
+
export const sqliteDbFactory = ({
|
|
47
|
+
sqlite3,
|
|
48
|
+
}: {
|
|
49
|
+
sqlite3: SQLiteAPI
|
|
50
|
+
}): Effect.Effect<
|
|
51
|
+
MakeSqliteDb<{ dbPointer: number; persistenceInfo: PersistenceInfo }, NodeDatabaseInput, NodeDatabaseMetadata>,
|
|
52
|
+
never,
|
|
53
|
+
FileSystem.FileSystem
|
|
54
|
+
> =>
|
|
55
|
+
Effect.andThen(
|
|
56
|
+
FileSystem.FileSystem,
|
|
57
|
+
(fs) => (input) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
if (input._tag === 'in-memory') {
|
|
60
|
+
const { dbPointer, vfs } = makeInMemoryDb(sqlite3)
|
|
61
|
+
return makeSqliteDb<NodeDatabaseMetadataInMemory>({
|
|
62
|
+
sqlite3,
|
|
63
|
+
metadata: {
|
|
64
|
+
_tag: 'in-memory',
|
|
65
|
+
vfs,
|
|
66
|
+
dbPointer,
|
|
67
|
+
persistenceInfo: { fileName: ':memory:' },
|
|
68
|
+
deleteDb: () => {},
|
|
69
|
+
configureDb: input.configureDb ?? (() => {}),
|
|
70
|
+
},
|
|
71
|
+
}) as any
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { dbPointer, vfs } = yield* makeNodeFsDb({
|
|
75
|
+
sqlite3,
|
|
76
|
+
fileName: input.fileName,
|
|
77
|
+
directory: input.directory,
|
|
78
|
+
fs,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const filePath = path.join(input.directory, input.fileName)
|
|
82
|
+
|
|
83
|
+
return makeSqliteDb<NodeDatabaseMetadataFs>({
|
|
84
|
+
sqlite3,
|
|
85
|
+
metadata: {
|
|
86
|
+
_tag: 'fs',
|
|
87
|
+
vfs,
|
|
88
|
+
dbPointer,
|
|
89
|
+
persistenceInfo: { fileName: input.fileName, directory: input.directory },
|
|
90
|
+
deleteDb: () => vfs.deleteDb(filePath),
|
|
91
|
+
configureDb: input.configureDb ?? (() => {}),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
let nodeFsVfs: NodeFS | undefined
|
|
98
|
+
|
|
99
|
+
const makeNodeFsDb = ({
|
|
100
|
+
sqlite3,
|
|
101
|
+
fileName,
|
|
102
|
+
directory,
|
|
103
|
+
fs,
|
|
104
|
+
}: {
|
|
105
|
+
sqlite3: WaSqlite.SQLiteAPI
|
|
106
|
+
fileName: string
|
|
107
|
+
directory: string
|
|
108
|
+
fs: FileSystem.FileSystem
|
|
109
|
+
}) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
// NOTE to keep the filePath short, we use the directory name in the vfs name
|
|
112
|
+
// If this is becoming a problem, we can use a hashed version of the directory name
|
|
113
|
+
const vfsName = `node-fs-${directory}`
|
|
114
|
+
if (nodeFsVfs === undefined) {
|
|
115
|
+
// TODO refactor with Effect FileSystem instead of using `node:fs` directly inside of NodeFS
|
|
116
|
+
nodeFsVfs = new NodeFS(vfsName, (sqlite3 as any).module, directory)
|
|
117
|
+
// @ts-expect-error TODO fix types
|
|
118
|
+
sqlite3.vfs_register(nodeFsVfs, false)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
yield* fs.makeDirectory(directory, { recursive: true })
|
|
122
|
+
|
|
123
|
+
const FILE_NAME_MAX_LENGTH = 56
|
|
124
|
+
if (fileName.length > FILE_NAME_MAX_LENGTH) {
|
|
125
|
+
throw new Error(`File name ${fileName} is too long. Maximum length is ${FILE_NAME_MAX_LENGTH} characters.`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// NOTE SQLite will return a "disk I/O error" if the file path is too long.
|
|
129
|
+
const dbPointer = sqlite3.open_v2Sync(fileName, undefined, vfsName)
|
|
130
|
+
|
|
131
|
+
return { dbPointer, vfs: nodeFsVfs }
|
|
132
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
package/tsconfig.json
ADDED