@ouim/vectoriadb-client 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 ADDED
@@ -0,0 +1,33 @@
1
+ import VectoriaDB from './index.js'
2
+
3
+ async function perfDemo() {
4
+ const db = new VectoriaDB({ serverUrl: 'http://localhost:3001' })
5
+
6
+ console.log('Initializing remote VectoriaDB (server should already be running)...')
7
+ await db.initialize()
8
+
9
+ console.log('Inserting 1,000 small documents in batches of 200...')
10
+ const docs = Array.from({ length: 1000 }, (_, i) => ({
11
+ id: `doc:${i}`,
12
+ text: `Document number ${i}`,
13
+ metadata: { id: `doc:${i}`, owner: 'demo' },
14
+ }))
15
+
16
+ const start = Date.now()
17
+ for (let i = 0; i < docs.length; i += 200) {
18
+ const batch = docs.slice(i, i + 200)
19
+ await db.addMany(batch)
20
+ }
21
+ console.log('Inserted 1000 docs in', Date.now() - start, 'ms')
22
+
23
+ console.log('Running a search...')
24
+ const results = await db.search('Document number 5', { topK: 10 })
25
+ console.log('Search results:', results.slice(0, 3))
26
+
27
+ db.close()
28
+ }
29
+
30
+ perfDemo().catch(err => {
31
+ console.error(err)
32
+ process.exit(1)
33
+ })
package/index.js ADDED
@@ -0,0 +1,172 @@
1
+ import SocketClient from './socket-client.js'
2
+
3
+ // Client SDK that mirrors VectoriaDB API surface (runtime validation + forwarding)
4
+ export default class VectoriaDB {
5
+ constructor(opts = {}) {
6
+ if (!opts.serverUrl) throw new Error('serverUrl is required')
7
+ this.serverUrl = opts.serverUrl
8
+ this.apiKey = opts.apiKey || null
9
+ this.requestTimeout = opts.requestTimeout || 30000
10
+ this._socket = new SocketClient({ serverUrl: this.serverUrl, apiKey: this.apiKey, requestTimeout: this.requestTimeout })
11
+ }
12
+
13
+ // --- helper to serialize function filters ---
14
+ static _serializeFunction(fn) {
15
+ if (!fn) return null
16
+ if (typeof fn === 'function') return { __isFnString: true, fn: fn.toString() }
17
+ if (typeof fn === 'string') return { __isFnString: true, fn: fn }
18
+ return null
19
+ }
20
+
21
+ // low-level forwarder
22
+ async _forward(method, ...params) {
23
+ return this._socket.sendRequest({ method, params })
24
+ }
25
+
26
+ // --- VectoriaDB API methods (as in docs) ---
27
+ async initialize() {
28
+ return this._forward('initialize')
29
+ }
30
+
31
+ async add(id, text, metadata) {
32
+ if (typeof id !== 'string') throw new TypeError('id must be string')
33
+ if (typeof text !== 'string') throw new TypeError('text must be string')
34
+ if (!metadata || typeof metadata !== 'object') throw new TypeError('metadata must be an object')
35
+ return this._forward('add', id, text, metadata)
36
+ }
37
+
38
+ async addMany(docs) {
39
+ if (!Array.isArray(docs)) throw new TypeError('docs must be an array')
40
+ if (docs.length === 0) return { added: 0 }
41
+ return this._forward('addMany', docs)
42
+ }
43
+
44
+ async has(id) {
45
+ return this._forward('has', id)
46
+ }
47
+
48
+ async get(id) {
49
+ return this._forward('get', id)
50
+ }
51
+
52
+ async size() {
53
+ return this._forward('size')
54
+ }
55
+
56
+ async update(id, updates, opts = {}) {
57
+ return this._forward('update', id, updates, opts)
58
+ }
59
+
60
+ async updateMetadata(id, metadata) {
61
+ return this._forward('updateMetadata', id, metadata)
62
+ }
63
+
64
+ async updateMany(updates) {
65
+ return this._forward('updateMany', updates)
66
+ }
67
+
68
+ async remove(id) {
69
+ return this._forward('remove', id)
70
+ }
71
+
72
+ async removeMany(ids) {
73
+ return this._forward('removeMany', ids)
74
+ }
75
+
76
+ async clear() {
77
+ return this._forward('clear')
78
+ }
79
+
80
+ async saveToStorage() {
81
+ return this._forward('saveToStorage')
82
+ }
83
+
84
+ async loadFromStorage() {
85
+ return this._forward('loadFromStorage')
86
+ }
87
+
88
+ async clearStorage() {
89
+ return this._forward('clearStorage')
90
+ }
91
+
92
+ async filter(fn) {
93
+ // filter is executed server-side — serialize function
94
+ if (typeof fn !== 'function') throw new TypeError('filter requires a function')
95
+ const serialized = VectoriaDB._serializeFunction(fn)
96
+ return this._forward('filter', serialized)
97
+ }
98
+
99
+ async search(queryOrVector, options = {}) {
100
+ const opts = { ...options }
101
+ if (opts.filter && typeof opts.filter === 'function') {
102
+ opts.filter = VectoriaDB._serializeFunction(opts.filter)
103
+ }
104
+ return this._forward('search', queryOrVector, opts)
105
+ }
106
+
107
+ // --- convenience collection-style API (maps to VectoriaDB primitives) ---
108
+ async createCollection(name) {
109
+ if (!name || typeof name !== 'string') throw new TypeError('collection name required')
110
+ // no-op on server side; client manages "collection" via metadata.owner
111
+ return { ok: true, collection: name }
112
+ }
113
+
114
+ async insert(collection, docs) {
115
+ if (!collection || typeof collection !== 'string') throw new TypeError('collection required')
116
+ if (!Array.isArray(docs)) throw new TypeError('docs must be an array')
117
+
118
+ const transformed = docs.map(d => {
119
+ const id = d.id || d.metadata?.id || `${collection}:${Math.random().toString(36).slice(2, 9)}`
120
+ const metadata = Object.assign({}, d.metadata || {}, { id, owner: collection })
121
+ return { id, text: d.text || d.vector?.toString() || (d.text ?? ''), metadata, vector: d.vector }
122
+ })
123
+
124
+ // Use addMany for textual documents; for vector-only docs we forward to addMany as well
125
+ return this.addMany(transformed)
126
+ }
127
+
128
+ async query(collection, queryVectorOrText, opts = {}) {
129
+ if (!collection || typeof collection !== 'string') throw new TypeError('collection required')
130
+ const options = { ...opts }
131
+ const originalFilter = options.filter
132
+
133
+ // Build a self-contained, serializable filter that enforces owner === collection
134
+ // and (optionally) runs the user's filter. We inline the user's filter source
135
+ // so the resulting function does not rely on outer closures when evaluated
136
+ // on the server process.
137
+ const ownerLiteral = JSON.stringify(collection)
138
+
139
+ if (originalFilter) {
140
+ // normalize original filter to a source string
141
+ let origFnStr
142
+ if (typeof originalFilter === 'function') {
143
+ origFnStr = originalFilter.toString()
144
+ } else if (typeof originalFilter === 'string') {
145
+ origFnStr = originalFilter
146
+ } else if (originalFilter && originalFilter.__isFnString && typeof originalFilter.fn === 'string') {
147
+ origFnStr = originalFilter.fn
148
+ } else {
149
+ throw new TypeError('filter must be a function or serialized function')
150
+ }
151
+
152
+ // inline user's filter so the combined function is self-contained server-side
153
+ const combinedFnStr = `(function(m){ try { const __orig = ${origFnStr}; return !!(__orig(m) && m && m.owner === ${ownerLiteral}); } catch(e) { return false } })`
154
+ options.filter = VectoriaDB._serializeFunction(combinedFnStr)
155
+ } else {
156
+ const ownerFnStr = `(function(m){ return m && m.owner === ${ownerLiteral}; })`
157
+ options.filter = VectoriaDB._serializeFunction(ownerFnStr)
158
+ }
159
+
160
+ return this.search(queryVectorOrText, options)
161
+ }
162
+
163
+ // close socket
164
+ close() {
165
+ try {
166
+ this._socket.close()
167
+ } catch (e) {}
168
+ }
169
+ }
170
+
171
+ // CommonJS fallback (so `require('./client')` still works in many setups)
172
+ export { VectoriaDB }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@ouim/vectoriadb-client",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "demo": "node demo.js",
8
+ "publish": "npm publish --access public"
9
+ },
10
+ "dependencies": {
11
+ "socket.io-client": "^4.8.0"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ }
16
+ }
@@ -0,0 +1,120 @@
1
+ import { io } from 'socket.io-client'
2
+
3
+ function _makeId() {
4
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
5
+ }
6
+
7
+ export default class SocketClient {
8
+ constructor({ serverUrl, namespace = '/vectoriadb', apiKey = null, requestTimeout = 30000 } = {}) {
9
+ if (!serverUrl) throw new Error('serverUrl is required')
10
+ this.serverUrl = serverUrl.replace(/\/$/, '')
11
+ this.namespace = namespace.startsWith('/') ? namespace : `/${namespace}`
12
+ this.url = `${this.serverUrl}${this.namespace}`
13
+ this.apiKey = apiKey
14
+ this.requestTimeout = requestTimeout || 30000
15
+
16
+ this.socket = io(this.url, {
17
+ auth: { apiKey },
18
+ path: '/socket.io',
19
+ transports: ['websocket'],
20
+ autoConnect: true,
21
+ reconnection: true,
22
+ })
23
+
24
+ this._pending = new Map() // id -> { resolve, reject, timer, chunks }
25
+ this._offlineQueue = []
26
+
27
+ this.socket.on('connect', () => {
28
+ // flush queue
29
+ while (this._offlineQueue.length) {
30
+ const payload = this._offlineQueue.shift()
31
+ this.socket.emit('request', payload)
32
+ }
33
+ })
34
+
35
+ this.socket.on('connect_error', err => {
36
+ // reject nothing here; pending requests will timeout or be retried
37
+ // console.warn('connect_error', err.message)
38
+ })
39
+
40
+ this.socket.on('disconnect', reason => {
41
+ // keep pending promises alive; they will timeout based on requestTimeout
42
+ })
43
+
44
+ this.socket.on('response', msg => {
45
+ const { id, result, error } = msg || {}
46
+ const pending = this._pending.get(id)
47
+ if (!pending) return
48
+
49
+ // If streamed chunks were received, assemble them in index order.
50
+ if (pending.chunksMap) {
51
+ const chunksMap = pending.chunksMap
52
+ const indexes = Object.keys(chunksMap)
53
+ .map(k => Number(k))
54
+ .sort((a, b) => a - b)
55
+ const assembled = indexes.flatMap(i => (Array.isArray(chunksMap[i]) ? chunksMap[i] : []))
56
+ clearTimeout(pending.timer)
57
+ pending.resolve(assembled)
58
+ this._pending.delete(id)
59
+ return
60
+ }
61
+
62
+ clearTimeout(pending.timer)
63
+ if (error) pending.reject(new Error(error.message || 'ServerError'))
64
+ else pending.resolve(result)
65
+ this._pending.delete(id)
66
+ })
67
+
68
+ this.socket.on('response-chunk', msg => {
69
+ const { id, chunk, index, totalChunks } = msg || {}
70
+ const pending = this._pending.get(id)
71
+ if (!pending) return
72
+
73
+ if (!pending.chunksMap) pending.chunksMap = {}
74
+ pending.chunksMap[index] = chunk
75
+ pending.receivedChunks = (pending.receivedChunks || 0) + 1
76
+ if (typeof totalChunks === 'number') pending.totalChunks = totalChunks
77
+
78
+ // reset timeout so long streams don't prematurely fail
79
+ if (pending.timer) {
80
+ clearTimeout(pending.timer)
81
+ pending.timer = setTimeout(() => {
82
+ this._pending.delete(id)
83
+ pending.reject(new Error('RequestTimeout'))
84
+ }, pending.timeoutMs || this.requestTimeout)
85
+ }
86
+ })
87
+ }
88
+
89
+ sendRequest({ method, params = [], collection = undefined, timeout = undefined } = {}) {
90
+ const id = _makeId()
91
+ const payload = { id, method, params, collection, timestamp: Date.now() }
92
+ const effectiveTimeout = typeof timeout === 'number' ? timeout : this.requestTimeout
93
+
94
+ const promise = new Promise((resolve, reject) => {
95
+ const timer = setTimeout(() => {
96
+ this._pending.delete(id)
97
+ reject(new Error('RequestTimeout'))
98
+ }, effectiveTimeout)
99
+
100
+ // store extra metadata so streaming chunks can reset the timer and be
101
+ // assembled in order on final response
102
+ this._pending.set(id, { resolve, reject, timer, timeoutMs: effectiveTimeout, chunksMap: null, receivedChunks: 0, totalChunks: null })
103
+
104
+ if (this.socket.connected) {
105
+ this.socket.emit('request', payload)
106
+ } else {
107
+ // queue for send on reconnect
108
+ this._offlineQueue.push(payload)
109
+ }
110
+ })
111
+
112
+ return promise
113
+ }
114
+
115
+ close() {
116
+ try {
117
+ this.socket.close()
118
+ } catch (e) {}
119
+ }
120
+ }