@ouim/vectoriadb-server 0.1.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.
- package/demo.js +23 -0
- package/index.js +23 -0
- package/package.json +20 -0
- package/vectoriadb-server.js +177 -0
package/demo.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import VectoriaDBServer from './vectoriadb-server.js'
|
|
2
|
+
import { FileStorageAdapter } from 'vectoriadb'
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = new VectoriaDBServer({
|
|
6
|
+
port: 3001,
|
|
7
|
+
vectoriadbConfig: {
|
|
8
|
+
storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/vectoriadb', namespace: 'default' }),
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
await server.listen()
|
|
13
|
+
console.log('Demo server is running. Connect with the client demo in the client folder.')
|
|
14
|
+
|
|
15
|
+
// graceful shutdown
|
|
16
|
+
process.on('SIGINT', async () => {
|
|
17
|
+
console.log('Shutting down...')
|
|
18
|
+
await server.close()
|
|
19
|
+
process.exit(0)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch(err => console.error(err))
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import VectoriaDBServer from './vectoriadb-server.js'
|
|
2
|
+
import { FileStorageAdapter } from 'vectoriadb'
|
|
3
|
+
|
|
4
|
+
// If run directly, start a demo server
|
|
5
|
+
if (process.argv[1] && process.argv[1].endsWith('index.js')) {
|
|
6
|
+
const server = new VectoriaDBServer({
|
|
7
|
+
port: process.env.PORT ? Number(process.env.PORT) : 3001,
|
|
8
|
+
host: '0.0.0.0',
|
|
9
|
+
vectoriadbConfig: {
|
|
10
|
+
// default storage / model - user should override for production
|
|
11
|
+
storageAdapter: new FileStorageAdapter({ cacheDir: '/data/vectoriadb', namespace: 'default' }),
|
|
12
|
+
},
|
|
13
|
+
cors: ['http://localhost:3000'],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
server.listen().catch(err => {
|
|
17
|
+
console.error(err)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { VectoriaDBServer }
|
|
23
|
+
export default VectoriaDBServer
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ouim/vectoriadb-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js",
|
|
8
|
+
"demo": "node demo.js",
|
|
9
|
+
"publish": "npm publish --access public"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@huggingface/transformers": "^3.8.1",
|
|
13
|
+
"socket.io": "^4.8.0",
|
|
14
|
+
"tslib": "^2.8.1",
|
|
15
|
+
"vectoriadb": "^2.1.3"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import { Server as IOServer } from 'socket.io'
|
|
3
|
+
|
|
4
|
+
// NOTE: server expects `vectoriadb` to be installed in the environment.
|
|
5
|
+
// It forwards calls to the real VectoriaDB instance.
|
|
6
|
+
import { VectoriaDB } from 'vectoriadb'
|
|
7
|
+
|
|
8
|
+
export default class VectoriaDBServer {
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.port = opts.port || 3001
|
|
11
|
+
this.host = opts.host || '0.0.0.0'
|
|
12
|
+
this.cors = opts.cors || []
|
|
13
|
+
this.apiKey = opts.apiKey || null
|
|
14
|
+
this.vectoriadbConfig = opts.vectoriadbConfig || {}
|
|
15
|
+
this.streamChunkSize = opts.streamChunkSize || 500
|
|
16
|
+
|
|
17
|
+
this._http = null
|
|
18
|
+
this._io = null
|
|
19
|
+
this._vectoria = null
|
|
20
|
+
this._sockets = new Set()
|
|
21
|
+
this._started = false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async listen() {
|
|
25
|
+
if (this._started) return
|
|
26
|
+
// Initialize VectoriaDB (loads models / storage as configured)
|
|
27
|
+
this._vectoria = new VectoriaDB(this.vectoriadbConfig)
|
|
28
|
+
if (typeof this._vectoria.initialize === 'function') {
|
|
29
|
+
await this._vectoria.initialize()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this._http = http.createServer((req, res) => {
|
|
33
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
|
34
|
+
res.end('VectoriaDB Server')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
this._io = new IOServer(this._http, {
|
|
38
|
+
cors: { origin: this.cors.length ? this.cors : '*', methods: ['GET', 'POST'] },
|
|
39
|
+
maxHttpBufferSize: 1e7,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const nsp = this._io.of('/vectoriadb')
|
|
43
|
+
|
|
44
|
+
nsp.use((socket, next) => {
|
|
45
|
+
// simple API key auth if configured
|
|
46
|
+
if (this.apiKey) {
|
|
47
|
+
const provided = socket.handshake.auth?.apiKey || socket.handshake.query?.apiKey
|
|
48
|
+
if (!provided || provided !== this.apiKey) {
|
|
49
|
+
return next(new Error('Unauthorized'))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
next()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
nsp.on('connection', socket => {
|
|
56
|
+
this._sockets.add(socket)
|
|
57
|
+
socket.on('disconnect', () => this._sockets.delete(socket))
|
|
58
|
+
|
|
59
|
+
socket.on('request', async payload => {
|
|
60
|
+
// payload: { id, method, params, collection, timestamp }
|
|
61
|
+
try {
|
|
62
|
+
await this._handleRequest(socket, payload)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
socket.emit('response', { id: payload?.id ?? null, result: null, error: { message: err.message }, took: 0 })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// allow ping from client
|
|
69
|
+
socket.on('health', cb => cb && cb({ ok: true, ts: Date.now() }))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await new Promise((res, rej) => {
|
|
73
|
+
this._http.listen(this.port, this.host, err => (err ? rej(err) : res()))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
this._started = true
|
|
77
|
+
console.log(`VectoriaDB Server listening on ${this.host}:${this.port}`)
|
|
78
|
+
return this
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async _handleRequest(socket, payload) {
|
|
82
|
+
if (!payload || typeof payload !== 'object') {
|
|
83
|
+
return socket.emit('response', { id: null, result: null, error: { message: 'Invalid payload' }, took: 0 })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { id, method, params = [] } = payload
|
|
87
|
+
const start = Date.now()
|
|
88
|
+
|
|
89
|
+
if (!method || typeof method !== 'string') {
|
|
90
|
+
return socket.emit('response', { id, result: null, error: { message: 'Missing method' }, took: Date.now() - start })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Allow passing serialized functions from client: convert stringified functions back to real functions
|
|
94
|
+
const reparsedParams = params.map(p => {
|
|
95
|
+
if (p && typeof p === 'object') {
|
|
96
|
+
// detect serialized function marker
|
|
97
|
+
if (p.__isFnString && typeof p.fn === 'string') {
|
|
98
|
+
// create function from string - executed in server process (trusted usage only)
|
|
99
|
+
// eslint-disable-next-line no-new-func
|
|
100
|
+
const fn = new Function('return (' + p.fn + ')')()
|
|
101
|
+
return fn
|
|
102
|
+
}
|
|
103
|
+
// also support options.filter as a string directly
|
|
104
|
+
if (typeof p.filter === 'string' && p.filter.trim().startsWith('function')) {
|
|
105
|
+
// eslint-disable-next-line no-new-func
|
|
106
|
+
p.filter = new Function('return (' + p.filter + ')')()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return p
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// dispatch to underlying VectoriaDB instance
|
|
114
|
+
const fn = this._vectoria[method]
|
|
115
|
+
if (typeof fn !== 'function') {
|
|
116
|
+
throw new Error(`MethodNotFound: ${method}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Run and capture result
|
|
120
|
+
const result = await Promise.race([
|
|
121
|
+
fn.apply(this._vectoria, reparsedParams),
|
|
122
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('ServerTimeout')), 30000)),
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
const took = Date.now() - start
|
|
126
|
+
|
|
127
|
+
// streaming support for very large arrays
|
|
128
|
+
if (Array.isArray(result) && result.length > this.streamChunkSize) {
|
|
129
|
+
const total = result.length
|
|
130
|
+
const chunkSize = this.streamChunkSize
|
|
131
|
+
for (let i = 0; i < total; i += chunkSize) {
|
|
132
|
+
const chunk = result.slice(i, i + chunkSize)
|
|
133
|
+
socket.emit('response-chunk', { id, chunk, index: Math.floor(i / chunkSize), totalChunks: Math.ceil(total / chunkSize) })
|
|
134
|
+
}
|
|
135
|
+
// final response indicating stream finished
|
|
136
|
+
return socket.emit('response', { id, result: { streamed: true, count: total }, error: null, took })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
socket.emit('response', { id, result, error: null, took })
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const took = Date.now() - start
|
|
142
|
+
socket.emit('response', { id, result: null, error: { message: err.message, name: err.name }, took })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async close() {
|
|
147
|
+
if (!this._started) return
|
|
148
|
+
// disconnect sockets
|
|
149
|
+
for (const s of Array.from(this._sockets)) {
|
|
150
|
+
try {
|
|
151
|
+
s.disconnect(true)
|
|
152
|
+
} catch (e) {}
|
|
153
|
+
}
|
|
154
|
+
this._sockets.clear()
|
|
155
|
+
|
|
156
|
+
if (this._io) {
|
|
157
|
+
try {
|
|
158
|
+
await this._io.close()
|
|
159
|
+
} catch (e) {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this._http) {
|
|
163
|
+
await new Promise(resolve => this._http.close(resolve))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// attempt to persist state if available
|
|
167
|
+
try {
|
|
168
|
+
if (this._vectoria && typeof this._vectoria.saveToStorage === 'function') {
|
|
169
|
+
await this._vectoria.saveToStorage()
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn('Error saving VectoriaDB state during shutdown:', err.message)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this._started = false
|
|
176
|
+
}
|
|
177
|
+
}
|