@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 CHANGED
@@ -113,13 +113,69 @@ const bobDocs = await db.query('my-collection', 'farewell', {
113
113
 
114
114
  ### Server Options
115
115
 
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` |
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
- 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
- })
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
- server.listen().catch(err => {
17
- console.error(err)
18
- process.exit(1)
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.2",
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",
@@ -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
- 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
- }
43
+ // fast-path: already started
44
+ if (this._started) return this
31
45
 
32
- this._http = http.createServer((req, res) => {
33
- res.writeHead(200, { 'Content-Type': 'text/plain' })
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._io = new IOServer(this._http, {
38
- cors: { origin: this.cors.length ? this.cors : '*', methods: ['GET', 'POST'] },
39
- maxHttpBufferSize: 1e7,
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
- const nsp = this._io.of('/vectoriadb')
57
+ this._http = http.createServer((req, res) => {
58
+ res.writeHead(200, { 'Content-Type': 'text/plain' })
59
+ res.end('VectoriaDB Server')
60
+ })
43
61
 
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
- })
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
- nsp.on('connection', socket => {
56
- this._sockets.add(socket)
57
- socket.on('disconnect', () => this._sockets.delete(socket))
67
+ const nsp = this._io.of('/vectoriadb')
58
68
 
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
- })
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
- // allow ping from client
69
- socket.on('health', cb => cb && cb({ ok: true, ts: Date.now() }))
70
- })
80
+ nsp.on('connection', socket => {
81
+ this._sockets.add(socket)
82
+ socket.on('disconnect', () => this._sockets.delete(socket))
71
83
 
72
- await new Promise((res, rej) => {
73
- this._http.listen(this.port, this.host, err => (err ? rej(err) : res()))
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
- this._started = true
77
- console.log(`VectoriaDB Server listening on ${this.host}:${this.port}`)
78
- return this
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 {