@ouim/vectoriadb-server 0.1.2 → 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/README.md +63 -16
- package/index.js +17 -17
- package/listen-race-test.js +28 -0
- package/package.json +2 -3
- package/vectoriadb-server.js +175 -45
package/README.md
CHANGED
|
@@ -113,13 +113,69 @@ const bobDocs = await db.query('my-collection', 'farewell', {
|
|
|
113
113
|
|
|
114
114
|
### Server Options
|
|
115
115
|
|
|
116
|
-
| Option
|
|
117
|
-
|
|
|
118
|
-
| `port`
|
|
119
|
-
| `host`
|
|
120
|
-
| `apiKey`
|
|
121
|
-
| `cors`
|
|
122
|
-
| `streamChunkSize`
|
|
116
|
+
| Option | Description | Default |
|
|
117
|
+
| :------------------------ | :------------------------------------------------------------------------------------- | :--------- |
|
|
118
|
+
| `port` | Server listening port | `3001` |
|
|
119
|
+
| `host` | Server host address | `0.0.0.0` |
|
|
120
|
+
| `apiKey` | Optional key for client authentication | `null` |
|
|
121
|
+
| `cors` | Allowed origins (array) | `[]` (All) |
|
|
122
|
+
| `streamChunkSize` | Max results per chunk for streaming | `500` |
|
|
123
|
+
| `autoSaveOnMutationBurst` | Enable automatic saveToStorage after a burst of mutation calls + idle | `true` |
|
|
124
|
+
| `autoSaveOnInactivity` | Save to storage after `mutationInactivityMs` of no mutations (suitable for small apps) | `true` |
|
|
125
|
+
| `mutationBurstThreshold` | Number of mutation calls within `mutationBurstWindowMs` to consider a burst | `5` |
|
|
126
|
+
| `mutationBurstWindowMs` | Time window (ms) used to count burst mutations | `120000` |
|
|
127
|
+
| `mutationInactivityMs` | Inactivity window (ms) after last mutation that triggers save | `30000` |
|
|
128
|
+
| `minSaveIntervalMs` | Minimum time (ms) between automatic saves to avoid thrashing | `10000` |
|
|
129
|
+
|
|
130
|
+
Auto-save on mutation bursts
|
|
131
|
+
|
|
132
|
+
A configurable mechanism that detects "mutation bursts" and automatically calls `saveToStorage()` after activity stops. This minimizes write amplification during heavy write periods while ensuring state is persisted shortly after the burst finishes.
|
|
133
|
+
|
|
134
|
+
How it works:
|
|
135
|
+
|
|
136
|
+
- A "burst" is detected when at least `mutationBurstThreshold` mutation operations occur within the `mutationBurstWindowMs` time window.
|
|
137
|
+
- When a burst has been detected, the server starts (or resets) an inactivity timer of `mutationInactivityMs` after the last mutation.
|
|
138
|
+
- If the inactivity timer expires and the recent mutation count still meets the threshold, the server calls `saveToStorage()` once.
|
|
139
|
+
- `minSaveIntervalMs` prevents repeated auto-saves from occurring too frequently; overlapping saves are also avoided by an internal in-progress flag.
|
|
140
|
+
- After the inactivity handler runs the mutation history is cleared until new activity occurs.
|
|
141
|
+
|
|
142
|
+
Behavior timeline (concrete example):
|
|
143
|
+
|
|
144
|
+
- Settings: `mutationBurstThreshold = 100`, `mutationBurstWindowMs = 120000` (2 min), `mutationInactivityMs = 30000` (30s).
|
|
145
|
+
- If 120 mutations arrive between t=0 and t=90s (burst detected), and no further mutations occur after t=90s, the server waits 30s (inactivity window) and calls `saveToStorage()` at ~t=120s (provided `minSaveIntervalMs` allows it).
|
|
146
|
+
|
|
147
|
+
Configuration example:
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
const server = new VectoriaDBServer({
|
|
151
|
+
port: 3001,
|
|
152
|
+
autoSaveOnMutationBurst: true,
|
|
153
|
+
mutationBurstThreshold: 100, // # mutations within the window to mark a burst
|
|
154
|
+
mutationBurstWindowMs: 2 * 60 * 1000, // window length used to count mutations
|
|
155
|
+
mutationInactivityMs: 30 * 1000, // wait this long after last mutation before saving
|
|
156
|
+
minSaveIntervalMs: 10 * 1000, // never auto-save more often than this
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Log & testing notes:
|
|
161
|
+
|
|
162
|
+
- When the auto-save runs the server logs: `[VectoriaDBServer] auto-saved storage (<reason>)` (e.g. `mutation-burst-inactivity` or `inactivity`).
|
|
163
|
+
- To test: simulate mutation calls and assert `saveToStorage()` is invoked after `mutationInactivityMs`; verify `minSaveIntervalMs` prevents frequent saves.
|
|
164
|
+
|
|
165
|
+
Inactivity-only mode (simpler)
|
|
166
|
+
|
|
167
|
+
If your application is small or you prefer a simpler policy, enable `autoSaveOnInactivity: true`. With this enabled the server will call `saveToStorage()` after `mutationInactivityMs` of no mutation activity — regardless of how many mutations occurred before the idle period. The inactivity timer resets on each mutation.
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const server = new VectoriaDBServer({
|
|
173
|
+
autoSaveOnInactivity: true,
|
|
174
|
+
mutationInactivityMs: 30 * 1000, // save 30s after last mutation
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Client Options
|
|
123
179
|
|
|
124
180
|
### Client Options
|
|
125
181
|
|
|
@@ -189,15 +245,6 @@ A: The client SDK stringifies your filter function. The server then reconstructs
|
|
|
189
245
|
|
|
190
246
|
---
|
|
191
247
|
|
|
192
|
-
## Future Roadmap
|
|
193
|
-
|
|
194
|
-
- [ ] **JWT Authentication**: Add more robust auth options.
|
|
195
|
-
- [ ] **Data Encryption**: Support for end-to-end encryption of metadata.
|
|
196
|
-
- [ ] **Multi-Instance**: Support for multiple VectoriaDB instances managed by one server.
|
|
197
|
-
- [ ] **Python Client**: A compatible SDK for Python applications.
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
248
|
## Credits
|
|
202
249
|
|
|
203
250
|
This SDK builds on and integrates with [VectoriaDB](https://github.com/agentfront/vectoriadb). See the Vectoria product website at [https://agentfront.dev/vectoria](https://agentfront.dev/vectoria) for more information and additional resources.
|
package/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import VectoriaDBServer from './vectoriadb-server.js'
|
|
2
2
|
import { FileStorageAdapter } from 'vectoriadb'
|
|
3
3
|
|
|
4
|
-
// If run directly, start a demo server
|
|
5
|
-
if (process.argv[1] && process.argv[1].endsWith('index.js')) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
16
|
+
// server.listen().catch(err => {
|
|
17
|
+
// console.error(err)
|
|
18
|
+
// process.exit(1)
|
|
19
|
+
// })
|
|
20
|
+
// }
|
|
21
21
|
|
|
22
|
-
export { VectoriaDBServer }
|
|
22
|
+
export { VectoriaDBServer, FileStorageAdapter }
|
|
23
23
|
export default VectoriaDBServer
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import VectoriaDBServer from './vectoriadb-server.js'
|
|
2
|
+
import { FileStorageAdapter } from 'vectoriadb'
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
const server = new VectoriaDBServer({
|
|
6
|
+
port: 3002,
|
|
7
|
+
vectoriadbConfig: {
|
|
8
|
+
storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/test-vectoriadb', namespace: 'test' }),
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// call listen() twice concurrently to reproduce the race if present
|
|
14
|
+
const results = await Promise.all([server.listen(), server.listen()])
|
|
15
|
+
console.log('listen() calls resolved, same instance:', results[0] === results[1])
|
|
16
|
+
console.log('server._started =', server._started)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('concurrent listen failed:', err)
|
|
19
|
+
process.exitCode = 1
|
|
20
|
+
} finally {
|
|
21
|
+
await server.close()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
run().catch(err => {
|
|
26
|
+
console.error(err)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouim/vectoriadb-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node index.js",
|
|
8
|
-
"demo": "node demo.js"
|
|
9
|
-
"publish": "npm publish --access public"
|
|
8
|
+
"demo": "node demo.js"
|
|
10
9
|
},
|
|
11
10
|
"dependencies": {
|
|
12
11
|
"@huggingface/transformers": "^3.8.1",
|
package/vectoriadb-server.js
CHANGED
|
@@ -14,68 +14,111 @@ export default class VectoriaDBServer {
|
|
|
14
14
|
this.vectoriadbConfig = opts.vectoriadbConfig || {}
|
|
15
15
|
this.streamChunkSize = opts.streamChunkSize || 500
|
|
16
16
|
|
|
17
|
+
// --- auto-save / burst-detection (configurable) ---
|
|
18
|
+
this.autoSaveOnMutationBurst = opts.autoSaveOnMutationBurst !== undefined ? !!opts.autoSaveOnMutationBurst : true
|
|
19
|
+
// when true the server will save after any inactivity period (see `mutationInactivityMs`) — useful for small apps
|
|
20
|
+
this.autoSaveOnInactivity = opts.autoSaveOnInactivity !== undefined ? !!opts.autoSaveOnInactivity : false
|
|
21
|
+
this.mutationBurstThreshold = Number(opts.mutationBurstThreshold) || 5
|
|
22
|
+
this.mutationBurstWindowMs = Number(opts.mutationBurstWindowMs) || 2 * 60 * 1000 // 2 minutes
|
|
23
|
+
this.mutationInactivityMs = Number(opts.mutationInactivityMs) || 30 * 1000 // 30s inactivity to trigger save
|
|
24
|
+
this.minSaveIntervalMs = Number(opts.minSaveIntervalMs) || 10 * 1000 // minimum time between auto-saves
|
|
25
|
+
|
|
26
|
+
// internal mutation-tracking state
|
|
27
|
+
this._mutationMethods = new Set(['add', 'addMany', 'update', 'remove', 'clear', 'insert', 'upsert', 'replace', 'put', 'delete'])
|
|
28
|
+
this._mutationTimestamps = []
|
|
29
|
+
this._inactivityTimer = null
|
|
30
|
+
this._lastBurstAt = 0
|
|
31
|
+
this._savingInProgress = false
|
|
32
|
+
this._lastSaveAt = 0
|
|
33
|
+
|
|
17
34
|
this._http = null
|
|
18
35
|
this._io = null
|
|
19
36
|
this._vectoria = null
|
|
20
37
|
this._sockets = new Set()
|
|
21
38
|
this._started = false
|
|
39
|
+
this._startPromise = null
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
async listen() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this._vectoria = new VectoriaDB(this.vectoriadbConfig)
|
|
28
|
-
if (typeof this._vectoria.initialize === 'function') {
|
|
29
|
-
await this._vectoria.initialize()
|
|
30
|
-
}
|
|
43
|
+
// fast-path: already started
|
|
44
|
+
if (this._started) return this
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
res.end('VectoriaDB Server')
|
|
35
|
-
})
|
|
46
|
+
// if a start is already in progress, return the same promise
|
|
47
|
+
if (this._startPromise) return this._startPromise
|
|
36
48
|
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
this._startPromise = (async () => {
|
|
50
|
+
try {
|
|
51
|
+
// Initialize VectoriaDB (loads models / storage as configured)
|
|
52
|
+
this._vectoria = new VectoriaDB(this.vectoriadbConfig)
|
|
53
|
+
if (typeof this._vectoria.initialize === 'function') {
|
|
54
|
+
await this._vectoria.initialize()
|
|
55
|
+
}
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
this._http = http.createServer((req, res) => {
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
|
59
|
+
res.end('VectoriaDB Server')
|
|
60
|
+
})
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!provided || provided !== this.apiKey) {
|
|
49
|
-
return next(new Error('Unauthorized'))
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
next()
|
|
53
|
-
})
|
|
62
|
+
this._io = new IOServer(this._http, {
|
|
63
|
+
cors: { origin: this.cors.length ? this.cors : '*', methods: ['GET', 'POST'] },
|
|
64
|
+
maxHttpBufferSize: 1e7,
|
|
65
|
+
})
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
this._sockets.add(socket)
|
|
57
|
-
socket.on('disconnect', () => this._sockets.delete(socket))
|
|
67
|
+
const nsp = this._io.of('/vectoriadb')
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
nsp.use((socket, next) => {
|
|
70
|
+
// simple API key auth if configured
|
|
71
|
+
if (this.apiKey) {
|
|
72
|
+
const provided = socket.handshake.auth?.apiKey || socket.handshake.query?.apiKey
|
|
73
|
+
if (!provided || provided !== this.apiKey) {
|
|
74
|
+
return next(new Error('Unauthorized'))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
next()
|
|
78
|
+
})
|
|
67
79
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
nsp.on('connection', socket => {
|
|
81
|
+
this._sockets.add(socket)
|
|
82
|
+
socket.on('disconnect', () => this._sockets.delete(socket))
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
socket.on('request', async payload => {
|
|
85
|
+
// payload: { id, method, params, collection, timestamp }
|
|
86
|
+
try {
|
|
87
|
+
await this._handleRequest(socket, payload)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
socket.emit('response', { id: payload?.id ?? null, result: null, error: { message: err.message }, took: 0 })
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// allow ping from client
|
|
94
|
+
socket.on('health', cb => cb && cb({ ok: true, ts: Date.now() }))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await new Promise((res, rej) => {
|
|
98
|
+
this._http.listen(this.port, this.host, err => (err ? rej(err) : res()))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
this._started = true
|
|
102
|
+
console.log(`VectoriaDB Server listening on ${this.host}:${this.port}`)
|
|
103
|
+
return this
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// cleanup partial resources on failure
|
|
106
|
+
try {
|
|
107
|
+
if (this._io) await this._io.close()
|
|
108
|
+
} catch (e) {}
|
|
109
|
+
try {
|
|
110
|
+
if (this._http) this._http.close(() => {})
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
})()
|
|
75
115
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
116
|
+
try {
|
|
117
|
+
return await this._startPromise
|
|
118
|
+
} finally {
|
|
119
|
+
// ensure the in-progress marker is cleared after attempt completes
|
|
120
|
+
this._startPromise = null
|
|
121
|
+
}
|
|
79
122
|
}
|
|
80
123
|
|
|
81
124
|
async _handleRequest(socket, payload) {
|
|
@@ -122,6 +165,15 @@ export default class VectoriaDBServer {
|
|
|
122
165
|
new Promise((_, reject) => setTimeout(() => reject(new Error('ServerTimeout')), 30000)),
|
|
123
166
|
])
|
|
124
167
|
|
|
168
|
+
// record mutation activity (used to auto-flush after a burst + inactivity)
|
|
169
|
+
if (this.autoSaveOnMutationBurst && this._isMutationMethod(method)) {
|
|
170
|
+
try {
|
|
171
|
+
this._recordMutation()
|
|
172
|
+
} catch (e) {
|
|
173
|
+
/* swallow tracking errors */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
const took = Date.now() - start
|
|
126
178
|
|
|
127
179
|
// streaming support for very large arrays
|
|
@@ -143,8 +195,86 @@ export default class VectoriaDBServer {
|
|
|
143
195
|
}
|
|
144
196
|
}
|
|
145
197
|
|
|
198
|
+
// --- Auto-save on mutation bursts (configurable) ---
|
|
199
|
+
_isMutationMethod(method) {
|
|
200
|
+
return this._mutationMethods.has(method)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_recordMutation() {
|
|
204
|
+
const now = Date.now()
|
|
205
|
+
this._mutationTimestamps.push(now)
|
|
206
|
+
// purge old timestamps outside the burst window
|
|
207
|
+
const cutoff = now - this.mutationBurstWindowMs
|
|
208
|
+
while (this._mutationTimestamps.length && this._mutationTimestamps[0] < cutoff) {
|
|
209
|
+
this._mutationTimestamps.shift()
|
|
210
|
+
}
|
|
211
|
+
if (this._mutationTimestamps.length >= this.mutationBurstThreshold) {
|
|
212
|
+
this._lastBurstAt = now
|
|
213
|
+
}
|
|
214
|
+
this._scheduleInactivityTimer()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_scheduleInactivityTimer() {
|
|
218
|
+
if (this._inactivityTimer) {
|
|
219
|
+
clearTimeout(this._inactivityTimer)
|
|
220
|
+
}
|
|
221
|
+
this._inactivityTimer = setTimeout(() => {
|
|
222
|
+
// fire-and-forget async
|
|
223
|
+
this._onInactivityTimeout().catch(err => console.warn('Inactivity flush error:', err.message))
|
|
224
|
+
}, this.mutationInactivityMs)
|
|
225
|
+
if (this._inactivityTimer.unref) this._inactivityTimer.unref()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async _onInactivityTimeout() {
|
|
229
|
+
this._inactivityTimer = null
|
|
230
|
+
const now = Date.now()
|
|
231
|
+
|
|
232
|
+
// Simple "inactivity-only" mode: save after any inactivity period if enabled.
|
|
233
|
+
if (this.autoSaveOnInactivity) {
|
|
234
|
+
if (this._mutationTimestamps.length > 0) {
|
|
235
|
+
await this._saveToStorage('inactivity')
|
|
236
|
+
}
|
|
237
|
+
// reset history and return early
|
|
238
|
+
this._mutationTimestamps = []
|
|
239
|
+
this._lastBurstAt = 0
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// count mutations within the burst window (existing burst-detection behavior)
|
|
244
|
+
const cutoff = now - this.mutationBurstWindowMs
|
|
245
|
+
const recentCount = this._mutationTimestamps.filter(ts => ts >= cutoff).length
|
|
246
|
+
if (recentCount >= this.mutationBurstThreshold) {
|
|
247
|
+
await this._saveToStorage('mutation-burst-inactivity')
|
|
248
|
+
}
|
|
249
|
+
// reset history (we only track bursts between inactivity windows)
|
|
250
|
+
this._mutationTimestamps = []
|
|
251
|
+
this._lastBurstAt = 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async _saveToStorage(reason = 'manual') {
|
|
255
|
+
if (!this._vectoria || typeof this._vectoria.saveToStorage !== 'function') return
|
|
256
|
+
const now = Date.now()
|
|
257
|
+
if (this._savingInProgress) return
|
|
258
|
+
if (now - this._lastSaveAt < this.minSaveIntervalMs) return
|
|
259
|
+
this._savingInProgress = true
|
|
260
|
+
try {
|
|
261
|
+
await this._vectoria.saveToStorage()
|
|
262
|
+
this._lastSaveAt = Date.now()
|
|
263
|
+
console.log(`[VectoriaDBServer] auto-saved storage (${reason})`)
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.warn('[VectoriaDBServer] auto-save failed:', err.message)
|
|
266
|
+
} finally {
|
|
267
|
+
this._savingInProgress = false
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
146
271
|
async close() {
|
|
147
272
|
if (!this._started) return
|
|
273
|
+
// stop any pending auto-save timer
|
|
274
|
+
if (this._inactivityTimer) {
|
|
275
|
+
clearTimeout(this._inactivityTimer)
|
|
276
|
+
this._inactivityTimer = null
|
|
277
|
+
}
|
|
148
278
|
// disconnect sockets
|
|
149
279
|
for (const s of Array.from(this._sockets)) {
|
|
150
280
|
try {
|