refactor(8.0)!: remove orphaned zero-config subsystem + dead cloud/progressive-init storage vestige
The old config-generation subsystem (src/config/ + autoConfiguration.ts) was
superseded during the 8.0 rework and never wired into init(): it emitted settings
for a partitioning subsystem that no longer exists and probed deleted cloud env
vars. The live zero-config path is inline — recall preset → HNSW knobs, storage
auto-detect, auto persistMode, container-memory-aware cache sizing.
The storage progressive-init / cloud-detection cluster was equally dead after the
cloud adapters were dropped: isCloudStorage() is permanently false (no overriders),
scheduleBackgroundInit/runBackgroundInit were never called (the latter an empty
body), initMode was never assigned, and Brainy.isFullyInitialized()/
awaitBackgroundInit() were always-trivial with zero callers. scheduleCountPersist()
collapses to its only-ever-taken immediate write-through path.
Removed:
- src/config/{index,zeroConfig,storageAutoConfig,modelAutoConfig,sharedConfigManager}.ts
- src/utils/autoConfiguration.ts + the inert BrainyZeroConfig export
- Brainy.isFullyInitialized()/awaitBackgroundInit() (+ BrainyInterface decls)
- InitMode type, isCloudStorage/detectCloudEnvironment/resolveInitMode,
scheduleBackgroundInit/runBackgroundInit/ensureValidatedForWrite and their state
- Dead cloud env-var probes (K_SERVICE/K_REVISION/AWS_LAMBDA_FUNCTION_NAME/
FUNCTIONS_TARGET/AZURE_FUNCTIONS_ENVIRONMENT)
Kept (verified live): production-detection logging (environment.ts), container-
memory cache sizing (memoryDetection/paramValidation), on-disk hash bucketing
(sharding.ts).
Docs: scrubbed deleted-subsystem references (JS quantization knobs, cloud/OPFS
adapters, partitioning, old zero-config API) across 14 files; deleted two wholly-
obsolete feature docs (complete-feature-list, v3-features); rewrote
architecture/zero-config for 8.0.
~3,700 LOC removed. Build clean; 1392 unit + 24 db-mvcc green.
This commit is contained in:
parent
00d3203d68
commit
35b9d7ef43
28 changed files with 596 additions and 3752 deletions
|
|
@ -1,15 +1,17 @@
|
|||
# Framework Integration Guide
|
||||
|
||||
Brainy 3.0 is **framework-first** - designed from the ground up to work seamlessly with modern JavaScript frameworks. This guide shows you how to integrate Brainy into any framework.
|
||||
Brainy is **framework-friendly** - designed to drop into the server side of any modern JavaScript framework. This guide shows you how to integrate Brainy into framework-based apps.
|
||||
|
||||
## 🎯 Why Framework-First?
|
||||
> **Runtime**: Brainy 8.0 runs on Node.js 22+ and Bun (server-side only). It is not a browser library. In framework apps, run Brainy in API routes, server components, server actions, loaders, or a dedicated backend service - never in client-side bundles.
|
||||
|
||||
Traditional AI databases require complex browser polyfills and bundler configurations. Brainy 3.0 trusts your framework to handle this:
|
||||
## 🎯 Why Server-Side?
|
||||
|
||||
Brainy embeds an HNSW vector index, a graph engine, and a filesystem-backed persistence layer. These belong on the server:
|
||||
|
||||
- **Zero configuration**: Just `import { Brainy } from '@soulcraft/brainy'`
|
||||
- **Framework responsibility**: Let Next.js, Vite, Webpack handle Node.js polyfills
|
||||
- **Cleaner code**: No browser-specific entry points or conditional imports
|
||||
- **Better DX**: Same API everywhere - browser, server, edge
|
||||
- **Auto storage detection**: `new Brainy()` auto-selects filesystem persistence on Node
|
||||
- **Cleaner code**: No browser polyfills, no conditional client/server imports
|
||||
- **Better DX**: One instance shared across your server routes
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
|
|
@ -24,7 +26,8 @@ npm install @soulcraft/brainy
|
|||
```javascript
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
// Works in any framework!
|
||||
// Run on the server (API route, server component, backend service)
|
||||
// new Brainy() auto-detects filesystem persistence on Node
|
||||
const brain = new Brainy()
|
||||
await brain.init()
|
||||
|
||||
|
|
@ -41,52 +44,48 @@ const results = await brain.find("framework integration")
|
|||
|
||||
## ⚛️ React Integration
|
||||
|
||||
Brainy runs on the server, so a React client component talks to it through an API endpoint (see the Next.js API route below). The hook fetches results; it never instantiates Brainy in the browser.
|
||||
|
||||
### Basic Hook Pattern
|
||||
|
||||
```jsx
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
function useBrainy() {
|
||||
const [brain, setBrain] = useState(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
function useBrainySearch(endpoint = '/api/search') {
|
||||
const [results, setResults] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const initBrain = async () => {
|
||||
const newBrain = new Brainy({
|
||||
storage: { type: 'opfs' } // Browser storage
|
||||
const search = useCallback(async (query) => {
|
||||
if (!query) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query })
|
||||
})
|
||||
await newBrain.init()
|
||||
setBrain(newBrain)
|
||||
setIsReady(true)
|
||||
const { results } = await res.json()
|
||||
setResults(results)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [endpoint])
|
||||
|
||||
initBrain()
|
||||
}, [])
|
||||
|
||||
return { brain, isReady }
|
||||
return { results, loading, search }
|
||||
}
|
||||
|
||||
// Usage in component
|
||||
function SearchComponent() {
|
||||
const { brain, isReady } = useBrainy()
|
||||
const [results, setResults] = useState([])
|
||||
|
||||
const handleSearch = useCallback(async (query) => {
|
||||
if (!isReady) return
|
||||
const searchResults = await brain.find(query)
|
||||
setResults(searchResults)
|
||||
}, [brain, isReady])
|
||||
|
||||
if (!isReady) return <div>Loading AI...</div>
|
||||
const { results, loading, search } = useBrainySearch()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
onChange={(e) => search(e.target.value)}
|
||||
/>
|
||||
{loading && <div>Searching...</div>}
|
||||
<div>
|
||||
{results.map(result => (
|
||||
<div key={result.id}>
|
||||
|
|
@ -100,48 +99,34 @@ function SearchComponent() {
|
|||
}
|
||||
```
|
||||
|
||||
### React Context Pattern
|
||||
### Shared Server Instance
|
||||
|
||||
```jsx
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
On the server, create one Brainy instance and reuse it across requests. This module is imported only by server code (API routes, server components), never by client components:
|
||||
|
||||
```javascript
|
||||
// lib/brain.server.js
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
const BrainyContext = createContext()
|
||||
let brainPromise
|
||||
|
||||
export function BrainyProvider({ children }) {
|
||||
const [brain, setBrain] = useState(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const initBrain = async () => {
|
||||
const newBrain = new Brainy()
|
||||
await newBrain.init()
|
||||
setBrain(newBrain)
|
||||
setIsReady(true)
|
||||
}
|
||||
|
||||
initBrain()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BrainyContext.Provider value={{ brain, isReady }}>
|
||||
{children}
|
||||
</BrainyContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useBrainContext() {
|
||||
const context = useContext(BrainyContext)
|
||||
if (!context) {
|
||||
throw new Error('useBrainContext must be used within BrainyProvider')
|
||||
export function getBrain() {
|
||||
if (!brainPromise) {
|
||||
brainPromise = (async () => {
|
||||
// new Brainy() auto-detects filesystem persistence on Node
|
||||
const brain = new Brainy()
|
||||
await brain.init()
|
||||
return brain
|
||||
})()
|
||||
}
|
||||
return context
|
||||
return brainPromise
|
||||
}
|
||||
```
|
||||
|
||||
## 🟢 Vue.js Integration
|
||||
|
||||
### Composition API
|
||||
Vue components call a server endpoint (see the Nuxt server route in the [Vue.js Integration Guide](vue-integration.md)); Brainy itself runs on the server.
|
||||
|
||||
### Composition API (client component)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
|
|
@ -155,100 +140,70 @@ export function useBrainContext() {
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const brain = ref(null)
|
||||
const isReady = ref(false)
|
||||
const query = ref('')
|
||||
const results = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
brain.value = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
await brain.value.init()
|
||||
isReady.value = true
|
||||
})
|
||||
|
||||
const search = async () => {
|
||||
if (!isReady.value || !query.value) return
|
||||
results.value = await brain.value.find(query.value)
|
||||
if (!query.value) return
|
||||
const res = await fetch('/api/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: query.value })
|
||||
})
|
||||
results.value = (await res.json()).results
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Vue 3 Plugin
|
||||
### Shared Server Instance
|
||||
|
||||
On the server, create one Brainy instance and reuse it across requests:
|
||||
|
||||
```javascript
|
||||
// plugins/brainy.js
|
||||
// server/brain.js (server-only module)
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
export default {
|
||||
install(app, options) {
|
||||
const brain = new Brainy(options)
|
||||
let brainPromise
|
||||
|
||||
app.config.globalProperties.$brain = brain
|
||||
app.provide('brain', brain)
|
||||
|
||||
// Initialize on app mount
|
||||
brain.init()
|
||||
export function getBrain() {
|
||||
if (!brainPromise) {
|
||||
brainPromise = (async () => {
|
||||
// new Brainy() auto-detects filesystem persistence on Node
|
||||
const brain = new Brainy()
|
||||
await brain.init()
|
||||
return brain
|
||||
})()
|
||||
}
|
||||
return brainPromise
|
||||
}
|
||||
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import BrainyPlugin from './plugins/brainy'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(BrainyPlugin, {
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
```
|
||||
|
||||
## 🅰️ Angular Integration
|
||||
|
||||
### Service Pattern
|
||||
The Angular service calls your backend over HTTP; Brainy lives in that backend, not in the browser.
|
||||
|
||||
### Service Pattern (calls the backend)
|
||||
|
||||
```typescript
|
||||
// brainy.service.ts
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BrainyService {
|
||||
private brain: Brainy
|
||||
private readySubject = new BehaviorSubject<boolean>(false)
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
ready$: Observable<boolean> = this.readySubject.asObservable()
|
||||
|
||||
constructor() {
|
||||
this.initBrain()
|
||||
search(query: string): Observable<{ results: any[] }> {
|
||||
return this.http.post<{ results: any[] }>('/api/search', { query })
|
||||
}
|
||||
|
||||
private async initBrain() {
|
||||
this.brain = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
await this.brain.init()
|
||||
this.readySubject.next(true)
|
||||
}
|
||||
|
||||
async search(query: string): Promise<any[]> {
|
||||
if (!this.readySubject.value) {
|
||||
throw new Error('Brain not ready')
|
||||
}
|
||||
return await this.brain.find(query)
|
||||
}
|
||||
|
||||
async add(data: any, type: string, metadata?: any): Promise<string> {
|
||||
if (!this.readySubject.value) {
|
||||
throw new Error('Brain not ready')
|
||||
}
|
||||
return await this.brain.add({ data, type, metadata })
|
||||
add(data: any, type: string, metadata?: any): Observable<{ id: string }> {
|
||||
return this.http.post<{ id: string }>('/api/add', { data, type, metadata })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -280,64 +235,52 @@ export class SearchComponent {
|
|||
|
||||
constructor(private brainyService: BrainyService) {}
|
||||
|
||||
async search() {
|
||||
search() {
|
||||
if (!this.query) return
|
||||
this.results = await this.brainyService.search(this.query)
|
||||
this.brainyService.search(this.query).subscribe(({ results }) => {
|
||||
this.results = results
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching backend endpoint uses Brainy directly (Node/Bun):
|
||||
|
||||
```typescript
|
||||
// server: api/search
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
const brain = new Brainy() // auto-detects filesystem persistence on Node
|
||||
await brain.init()
|
||||
|
||||
export async function handleSearch(query: string) {
|
||||
return await brain.find(query)
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Next.js Integration
|
||||
|
||||
### App Router (Next.js 13+)
|
||||
In Next.js, Brainy lives in server code only: API routes, server components, or server actions. Create one shared instance in a server-only module.
|
||||
|
||||
```jsx
|
||||
// app/providers.jsx
|
||||
'use client'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
### Shared Server Instance
|
||||
|
||||
```javascript
|
||||
// lib/brain.server.js (imported only by server code)
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
const BrainyContext = createContext()
|
||||
let brainPromise
|
||||
|
||||
export function BrainyProvider({ children }) {
|
||||
const [brain, setBrain] = useState(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const initBrain = async () => {
|
||||
const newBrain = new Brainy()
|
||||
await newBrain.init()
|
||||
setBrain(newBrain)
|
||||
setIsReady(true)
|
||||
}
|
||||
|
||||
initBrain()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BrainyContext.Provider value={{ brain, isReady }}>
|
||||
{children}
|
||||
</BrainyContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useBrainy = () => useContext(BrainyContext)
|
||||
```
|
||||
|
||||
```jsx
|
||||
// app/layout.jsx
|
||||
import { BrainyProvider } from './providers'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<BrainyProvider>
|
||||
{children}
|
||||
</BrainyProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
export function getBrain() {
|
||||
if (!brainPromise) {
|
||||
brainPromise = (async () => {
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'filesystem', rootDirectory: './data' }
|
||||
})
|
||||
await brain.init()
|
||||
return brain
|
||||
})()
|
||||
}
|
||||
return brainPromise
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -345,45 +288,78 @@ export default function RootLayout({ children }) {
|
|||
|
||||
```javascript
|
||||
// app/api/search/route.js
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'filesystem', rootDirectory: './data' }
|
||||
})
|
||||
await brain.init()
|
||||
import { getBrain } from '@/lib/brain.server'
|
||||
|
||||
export async function POST(request) {
|
||||
const { query } = await request.json()
|
||||
const brain = await getBrain()
|
||||
const results = await brain.find(query)
|
||||
|
||||
return Response.json({ results })
|
||||
}
|
||||
```
|
||||
|
||||
## 🔷 Svelte Integration
|
||||
### Server Action
|
||||
|
||||
```javascript
|
||||
// app/actions.js
|
||||
'use server'
|
||||
import { getBrain } from '@/lib/brain.server'
|
||||
|
||||
export async function search(query) {
|
||||
const brain = await getBrain()
|
||||
return await brain.find(query)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔷 SvelteKit Integration
|
||||
|
||||
Brainy runs in a server-only module (`*.server.js`); the component fetches results from an endpoint.
|
||||
|
||||
```javascript
|
||||
// src/lib/server/brain.js (server-only — note the .server suffix)
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
let brainPromise
|
||||
|
||||
export function getBrain() {
|
||||
if (!brainPromise) {
|
||||
brainPromise = (async () => {
|
||||
const brain = new Brainy() // auto-detects filesystem persistence
|
||||
await brain.init()
|
||||
return brain
|
||||
})()
|
||||
}
|
||||
return brainPromise
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// src/routes/api/search/+server.js
|
||||
import { json } from '@sveltejs/kit'
|
||||
import { getBrain } from '$lib/server/brain'
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { query } = await request.json()
|
||||
const brain = await getBrain()
|
||||
return json({ results: await brain.find(query) })
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- SearchComponent.svelte -->
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
let brain = null
|
||||
let isReady = false
|
||||
let query = ''
|
||||
let results = []
|
||||
|
||||
onMount(async () => {
|
||||
brain = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
await brain.init()
|
||||
isReady = true
|
||||
})
|
||||
|
||||
async function search() {
|
||||
if (!isReady || !query) return
|
||||
results = await brain.find(query)
|
||||
if (!query) return
|
||||
const res = await fetch('/api/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query })
|
||||
})
|
||||
results = (await res.json()).results
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -401,27 +377,23 @@ export async function POST(request) {
|
|||
|
||||
## 🌟 Solid.js Integration
|
||||
|
||||
The component calls a server endpoint (use SolidStart server routes, or any backend, to host Brainy):
|
||||
|
||||
```jsx
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { createSignal } from 'solid-js'
|
||||
|
||||
function SearchComponent() {
|
||||
const [brain, setBrain] = createSignal(null)
|
||||
const [isReady, setIsReady] = createSignal(false)
|
||||
const [query, setQuery] = createSignal('')
|
||||
const [results, setResults] = createSignal([])
|
||||
|
||||
onMount(async () => {
|
||||
const newBrain = new Brainy()
|
||||
await newBrain.init()
|
||||
setBrain(newBrain)
|
||||
setIsReady(true)
|
||||
})
|
||||
|
||||
const search = async () => {
|
||||
if (!isReady() || !query()) return
|
||||
const searchResults = await brain().find(query())
|
||||
setResults(searchResults)
|
||||
if (!query()) return
|
||||
const res = await fetch('/api/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: query() })
|
||||
})
|
||||
setResults((await res.json()).results)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -450,57 +422,25 @@ function SearchComponent() {
|
|||
|
||||
## 📦 Bundler Configuration
|
||||
|
||||
### Vite (Recommended)
|
||||
Brainy is a server-side dependency, so keep it out of client bundles. Import it only from server-only modules (`*.server.js`, API routes, server components, server actions). If your bundler ever tries to pull Brainy into a client bundle, that's a sign it's being imported from a client component — move the import to a server module.
|
||||
|
||||
For server builds, mark Brainy as external so the bundler doesn't inline it:
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
// vite.config.js (SSR build)
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@soulcraft/brainy']
|
||||
ssr: {
|
||||
external: ['@soulcraft/brainy']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Webpack
|
||||
|
||||
```javascript
|
||||
// webpack.config.js
|
||||
module.exports = {
|
||||
resolve: {
|
||||
fallback: {
|
||||
"fs": false,
|
||||
"path": require.resolve("path-browserify"),
|
||||
"crypto": require.resolve("crypto-browserify")
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
global: 'global'
|
||||
})
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Rollup
|
||||
|
||||
```javascript
|
||||
// rollup.config.js
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
|
||||
// rollup.config.js (server bundle)
|
||||
export default {
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
external: ['@soulcraft/brainy', 'node:fs', 'node:path', 'node:crypto']
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -508,30 +448,24 @@ export default {
|
|||
|
||||
### Server-Side Rendering
|
||||
|
||||
Instantiate Brainy on the server and feed its results into the rendered page. Never construct it in client code.
|
||||
|
||||
```javascript
|
||||
// Check if running in browser
|
||||
if (typeof window !== 'undefined') {
|
||||
// Browser-only code
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
}
|
||||
// Server-side data loading (framework loader / getServerSideProps / load fn)
|
||||
import { getBrain } from './brain.server'
|
||||
|
||||
// Or use dynamic imports
|
||||
const initBrainForBrowser = async () => {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const { Brainy } = await import('@soulcraft/brainy')
|
||||
const brain = new Brainy()
|
||||
await brain.init()
|
||||
return brain
|
||||
export async function load({ url }) {
|
||||
const brain = await getBrain()
|
||||
const query = url.searchParams.get('q') ?? ''
|
||||
const results = query ? await brain.find(query) : []
|
||||
return { results }
|
||||
}
|
||||
```
|
||||
|
||||
### Static Site Generation
|
||||
|
||||
```javascript
|
||||
// For build-time usage
|
||||
// For build-time usage (runs in Node during the build)
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
export async function generateStaticProps() {
|
||||
|
|
@ -552,67 +486,46 @@ export async function generateStaticProps() {
|
|||
## 🔧 Framework-Specific Tips
|
||||
|
||||
### React
|
||||
- Use `useCallback` for search functions to prevent re-renders
|
||||
- Consider `useMemo` for expensive brain operations
|
||||
- Implement cleanup in `useEffect` for proper memory management
|
||||
- Keep components client-side and call a Brainy-backed API route
|
||||
- Use `useCallback` for fetch handlers to prevent re-renders
|
||||
- Debounce keystroke-driven searches before hitting the endpoint
|
||||
|
||||
### Vue
|
||||
- Use `shallowRef` for the brain instance (it's not reactive data)
|
||||
- Consider Pinia for global brain state management
|
||||
- Use `watchEffect` for reactive search queries
|
||||
- Components call an endpoint; the shared instance lives in a server module
|
||||
- Consider Pinia for caching results client-side
|
||||
- Debounce reactive search queries
|
||||
|
||||
### Angular
|
||||
- Implement proper dependency injection with services
|
||||
- Use RxJS observables for reactive search
|
||||
- Consider lazy loading brain in feature modules
|
||||
- Use `HttpClient` and RxJS to call the backend
|
||||
- Hold the shared Brainy instance in your Node backend, not the app
|
||||
- Consider lazy loading search features in feature modules
|
||||
|
||||
### Next.js
|
||||
- Use dynamic imports for client-side only features
|
||||
- Consider API routes for server-side brain operations
|
||||
- Implement proper error boundaries
|
||||
- Put Brainy in server-only modules (`*.server.js`), API routes, or server actions
|
||||
- Reuse one shared instance across requests
|
||||
- Implement proper error boundaries for failed fetches
|
||||
|
||||
## 🚨 Common Issues & Solutions
|
||||
|
||||
### Issue: "crypto is not defined"
|
||||
**Solution**: Your framework should handle this automatically. If not:
|
||||
```javascript
|
||||
// Add to your bundle config
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
}
|
||||
```
|
||||
### Issue: "fs module not found" / "crypto is not defined" in the browser
|
||||
**Cause**: Brainy was imported into a client bundle. Brainy 8.0 is a server-side library (Node 22+/Bun) and uses Node built-ins like `fs` and `crypto`.
|
||||
**Solution**: Import Brainy only from server code — server-only modules (`*.server.js`), API routes, server components, or server actions. From client components, call those endpoints instead.
|
||||
|
||||
### Issue: "fs module not found"
|
||||
**Solution**: This is expected in browsers. Use browser-compatible storage:
|
||||
```javascript
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'opfs' } // Or 'memory' for development
|
||||
})
|
||||
```
|
||||
|
||||
### Issue: Large bundle size
|
||||
**Solution**: Use dynamic imports for optional features:
|
||||
```javascript
|
||||
const brain = await import('@soulcraft/brainy').then(m => new m.Brainy())
|
||||
```
|
||||
### Issue: Large client bundle size
|
||||
**Cause**: A client module is pulling in Brainy.
|
||||
**Solution**: Move the `import { Brainy } from '@soulcraft/brainy'` into a server-only module so it never reaches the browser bundle.
|
||||
|
||||
### Issue: SSR hydration mismatch
|
||||
**Solution**: Initialize brain only on client:
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
// Browser-only initialization
|
||||
initBrain()
|
||||
}, [])
|
||||
```
|
||||
**Solution**: Run the search on the server (loader / server action / API route) and pass the results down as props, so server and client render the same markup.
|
||||
|
||||
## 🎯 Best Practices
|
||||
|
||||
1. **Initialize Once**: Create brain instance at app level, not component level
|
||||
2. **Use Context**: Share brain instance across components with context/providers
|
||||
3. **Handle Loading**: Always show loading states during brain initialization
|
||||
4. **Error Boundaries**: Implement proper error handling for brain operations
|
||||
5. **Memory Management**: Clean up brain instances on unmount
|
||||
6. **Storage Strategy**: Choose appropriate storage for your deployment target
|
||||
1. **Initialize Once**: Create one shared Brainy instance per server process, not per request
|
||||
2. **Server-Only**: Import Brainy only from server modules — never from client components
|
||||
3. **Endpoint Boundary**: Expose search/add through API routes or server actions
|
||||
4. **Handle Loading**: Show loading states in the client while the fetch is in flight
|
||||
5. **Error Handling**: Catch and surface failed endpoint calls gracefully
|
||||
6. **Storage**: Use `filesystem` for persistence (the default on Node) or `memory` for ephemeral/tests
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -1743,7 +1743,7 @@ For off-site backup, snapshot `rootDirectory` from your scheduler with `gsutil r
|
|||
- HNSW Index: O(log n) search (1B entities = ~30 hops)
|
||||
- Metadata Index: O(1) filtering
|
||||
- Graph Adjacency: O(1) relationship lookups
|
||||
- Storage: Unlimited (cloud buckets)
|
||||
- Storage: Bounded by the filesystem volume backing `rootDirectory`
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
|
|
@ -1887,7 +1887,7 @@ groupBy: 'type'
|
|||
- ✅ O(1) metadata filtering
|
||||
- ✅ O(1) relationship traversal
|
||||
- ✅ Human-readable VFS structure
|
||||
- ✅ Cloud storage support (GCS/S3/R2)
|
||||
- ✅ Filesystem-backed persistence (snapshot/sync the directory for off-site backup)
|
||||
- ✅ Billion-scale performance
|
||||
- ✅ Zero mocks, production-ready!
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ console.log('Brainy ready.')
|
|||
|
||||
## Native Acceleration (Optional)
|
||||
|
||||
For production workloads, add Cortex for Rust-accelerated SIMD distance calculations, vector quantization, and native embeddings:
|
||||
For production workloads, add Cortex for Rust-accelerated SIMD distance calculations and native embeddings:
|
||||
|
||||
```bash
|
||||
npm install @soulcraft/cortex
|
||||
|
|
|
|||
|
|
@ -108,4 +108,4 @@ await brain.find({ query: 'people who work at Anthropic' })
|
|||
- [Triple Intelligence](/docs/concepts/triple-intelligence) — understand how the query engine works
|
||||
- [The Find System](/docs/guides/find-system) — advanced queries, operators, and graph traversal
|
||||
- [API Reference](/docs/api/reference) — complete method documentation
|
||||
- [Storage Adapters](/docs/guides/storage-adapters) — S3, GCS, Azure, filesystem, OPFS
|
||||
- [Storage Adapters](/docs/guides/storage-adapters) — filesystem, memory
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ Progress is reported after each batch (batch size is determined by the storage a
|
|||
|
||||
## Storage Backend Compatibility
|
||||
|
||||
Migrations work identically across all storage backends (Memory, FileSystem, S3, R2, GCS, OPFS). The system uses `BaseStorage` methods (`getNouns`, `saveNounMetadata`, `getVerbs`, `saveVerbMetadata`) which are implemented by every adapter.
|
||||
Migrations work identically across all storage backends (Memory, FileSystem). The system uses `BaseStorage` methods (`getNouns`, `saveNounMetadata`, `getVerbs`, `saveVerbMetadata`) which are implemented by every adapter.
|
||||
|
||||
Batch size and rate limiting are automatically configured per adapter — no tuning required.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Complete guide to integrating Brainy with Vue.js applications, covering Vue 3, Nuxt.js, Composition API, Options API, and advanced patterns.
|
||||
|
||||
> **Runtime**: Brainy 8.0 runs on Node.js 22+ and Bun, server-side only — it is not a browser library. In Vue apps, Brainy lives behind an HTTP endpoint (a Nuxt server route, or any Node/Bun backend), and your components call that endpoint. The composables, plugin, and store below are wrappers around `fetch`; the single Brainy instance is created once on the server (see [Nuxt Server Route](#nuxt-server-route)).
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Installation
|
||||
|
|
@ -15,34 +17,47 @@ npm install @soulcraft/brainy
|
|||
|
||||
### Basic Setup
|
||||
|
||||
The composable talks to a Brainy-backed endpoint (see [Nuxt Server Route](#nuxt-server-route)). It is always "ready" because there is no client-side initialization — the server owns the Brainy instance.
|
||||
|
||||
```javascript
|
||||
// src/composables/useBrainy.js
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const brain = ref(null)
|
||||
const isReady = ref(false)
|
||||
const error = ref(null)
|
||||
export function useBrainy(endpoint = '/api/brain') {
|
||||
const error = ref(null)
|
||||
|
||||
export function useBrainy() {
|
||||
onMounted(async () => {
|
||||
const search = async (query, options = {}) => {
|
||||
error.value = null
|
||||
try {
|
||||
brain.value = new Brainy({
|
||||
storage: { type: 'opfs' } // Browser storage
|
||||
const res = await fetch(`${endpoint}/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, options })
|
||||
})
|
||||
await brain.value.init()
|
||||
isReady.value = true
|
||||
if (!res.ok) throw new Error(`Search failed: ${res.status}`)
|
||||
return (await res.json()).results
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Brainy initialization failed:', err)
|
||||
console.error('Brainy search failed:', err)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
brain: readonly(brain),
|
||||
isReady: readonly(isReady),
|
||||
error: readonly(error)
|
||||
}
|
||||
|
||||
const add = async (data, type, metadata) => {
|
||||
const res = await fetch(`${endpoint}/add`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data, type, metadata })
|
||||
})
|
||||
return (await res.json()).id
|
||||
}
|
||||
|
||||
const stats = async () => {
|
||||
const res = await fetch(`${endpoint}/stats`)
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
return { search, add, stats, error: readonly(error) }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -54,43 +69,36 @@ export function useBrainy() {
|
|||
<!-- src/components/Search.vue -->
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<div v-if="!isReady" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Initializing AI...</span>
|
||||
<div class="search-input">
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search..."
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="search-input">
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search with AI..."
|
||||
class="input"
|
||||
/>
|
||||
<div v-if="loading" class="loading">
|
||||
Searching...
|
||||
</div>
|
||||
|
||||
<div v-else class="results">
|
||||
<div
|
||||
v-for="result in results"
|
||||
:key="result.id"
|
||||
class="result-item"
|
||||
>
|
||||
<h3>{{ result.data }}</h3>
|
||||
<div class="result-meta">
|
||||
<span>Score: {{ (result.score * 100).toFixed(1) }}%</span>
|
||||
<span v-if="result.metadata?.type">
|
||||
Type: {{ result.metadata.type }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
Searching...
|
||||
</div>
|
||||
|
||||
<div v-else class="results">
|
||||
<div
|
||||
v-for="result in results"
|
||||
:key="result.id"
|
||||
class="result-item"
|
||||
>
|
||||
<h3>{{ result.data }}</h3>
|
||||
<div class="result-meta">
|
||||
<span>Score: {{ (result.score * 100).toFixed(1) }}%</span>
|
||||
<span v-if="result.metadata?.type">
|
||||
Type: {{ result.metadata.type }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="query && !loading && results.length === 0" class="no-results">
|
||||
No results found for "{{ query }}"
|
||||
</div>
|
||||
<div v-if="query && !loading && results.length === 0" class="no-results">
|
||||
No results found for "{{ query }}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -101,22 +109,21 @@ import { ref, watch } from 'vue'
|
|||
import { useBrainy } from '../composables/useBrainy'
|
||||
import { debounce } from '../utils/debounce'
|
||||
|
||||
const { brain, isReady } = useBrainy()
|
||||
const { search: searchBrain } = useBrainy()
|
||||
|
||||
const query = ref('')
|
||||
const results = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const search = async (searchQuery) => {
|
||||
if (!isReady.value || !searchQuery.trim()) {
|
||||
if (!searchQuery.trim()) {
|
||||
results.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const searchResults = await brain.value.find(searchQuery)
|
||||
results.value = searchResults
|
||||
results.value = await searchBrain(searchQuery)
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
results.value = []
|
||||
|
|
@ -228,11 +235,7 @@ watch(query, (newQuery) => {
|
|||
<div class="data-manager">
|
||||
<h2>Data Management</h2>
|
||||
|
||||
<div v-if="!isReady" class="loading">
|
||||
Initializing...
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div>
|
||||
<!-- Add Data Form -->
|
||||
<form @submit.prevent="addData" class="add-form">
|
||||
<h3>Add New Data</h3>
|
||||
|
|
@ -289,7 +292,7 @@ watch(query, (newQuery) => {
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useBrainy } from '../composables/useBrainy'
|
||||
|
||||
const { brain, isReady } = useBrainy()
|
||||
const { add, stats: fetchStats } = useBrainy()
|
||||
|
||||
const newItem = reactive({
|
||||
data: '',
|
||||
|
|
@ -299,18 +302,7 @@ const newItem = reactive({
|
|||
|
||||
const stats = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (isReady.value) {
|
||||
await loadStats()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for brain readiness
|
||||
watch(isReady, async (ready) => {
|
||||
if (ready) {
|
||||
await loadStats()
|
||||
}
|
||||
})
|
||||
onMounted(loadStats)
|
||||
|
||||
const addData = async () => {
|
||||
try {
|
||||
|
|
@ -319,11 +311,7 @@ const addData = async () => {
|
|||
tags: newItem.tags.split(',').map(tag => tag.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
await brain.value.add({
|
||||
data: newItem.data,
|
||||
type: newItem.type,
|
||||
metadata
|
||||
})
|
||||
await add(newItem.data, newItem.type, metadata)
|
||||
|
||||
// Reset form
|
||||
newItem.data = ''
|
||||
|
|
@ -339,7 +327,7 @@ const addData = async () => {
|
|||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
stats.value = await brain.value.stats()
|
||||
stats.value = await fetchStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error)
|
||||
}
|
||||
|
|
@ -435,73 +423,55 @@ button:disabled {
|
|||
<!-- src/components/SearchOptions.vue -->
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<div v-if="!isReady" class="loading">
|
||||
Initializing AI...
|
||||
</div>
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
<div v-if="loading" class="loading">Searching...</div>
|
||||
|
||||
<div v-if="loading" class="loading">Searching...</div>
|
||||
|
||||
<div class="results">
|
||||
<div
|
||||
v-for="result in results"
|
||||
:key="result.id"
|
||||
class="result-item"
|
||||
>
|
||||
<h3>{{ result.data }}</h3>
|
||||
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
<div class="results">
|
||||
<div
|
||||
v-for="result in results"
|
||||
:key="result.id"
|
||||
class="result-item"
|
||||
>
|
||||
<h3>{{ result.data }}</h3>
|
||||
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { debounce } from '../utils/debounce'
|
||||
|
||||
export default {
|
||||
name: 'SearchOptions',
|
||||
data() {
|
||||
return {
|
||||
brain: null,
|
||||
isReady: false,
|
||||
query: '',
|
||||
results: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.initBrain()
|
||||
},
|
||||
methods: {
|
||||
async initBrain() {
|
||||
try {
|
||||
this.brain = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
await this.brain.init()
|
||||
this.isReady = true
|
||||
} catch (error) {
|
||||
console.error('Brain initialization failed:', error)
|
||||
}
|
||||
},
|
||||
async search(query) {
|
||||
if (!this.isReady || !query.trim()) {
|
||||
if (!query.trim()) {
|
||||
this.results = []
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
this.results = await this.brain.find(query)
|
||||
const res = await fetch('/api/brain/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query })
|
||||
})
|
||||
this.results = (await res.json()).results
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
this.results = []
|
||||
|
|
@ -520,10 +490,6 @@ export default {
|
|||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Cleanup if needed
|
||||
this.brain = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -533,31 +499,22 @@ export default {
|
|||
|
||||
### Global Brainy Plugin
|
||||
|
||||
The plugin exposes a global `$searchBrain` helper that calls your Brainy-backed endpoint. Brainy itself runs on the server (see [Nuxt Server Route](#nuxt-server-route)).
|
||||
|
||||
```javascript
|
||||
// src/plugins/brainy.js
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
export default {
|
||||
install(app, options = {}) {
|
||||
const defaultOptions = {
|
||||
storage: { type: 'opfs' },
|
||||
...options
|
||||
}
|
||||
const endpoint = options.endpoint ?? '/api/brain'
|
||||
|
||||
const brain = new Brainy(defaultOptions)
|
||||
|
||||
// Make brain available globally
|
||||
app.config.globalProperties.$brain = brain
|
||||
app.provide('brain', brain)
|
||||
|
||||
// Auto-initialize
|
||||
brain.init().catch(error => {
|
||||
console.error('Brainy plugin initialization failed:', error)
|
||||
})
|
||||
|
||||
// Add global method
|
||||
app.config.globalProperties.$searchBrain = async (query, options) => {
|
||||
return await brain.find(query, options)
|
||||
// Add global search method (calls the server endpoint)
|
||||
app.config.globalProperties.$searchBrain = async (query, searchOptions) => {
|
||||
const res = await fetch(`${endpoint}/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, options: searchOptions })
|
||||
})
|
||||
return (await res.json()).results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -574,7 +531,7 @@ import BrainyPlugin from './plugins/brainy'
|
|||
const app = createApp(App)
|
||||
|
||||
app.use(BrainyPlugin, {
|
||||
storage: { type: 'opfs' }
|
||||
endpoint: '/api/brain'
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
|
|
@ -611,24 +568,58 @@ export default {
|
|||
|
||||
## 🏰 Nuxt.js Integration
|
||||
|
||||
### Nuxt Plugin
|
||||
Nuxt's server engine (Nitro) is the natural home for Brainy: it runs on Node/Bun, so the Brainy instance lives in `server/`, and pages/components call its routes.
|
||||
|
||||
### Nuxt Server Route
|
||||
|
||||
```javascript
|
||||
// plugins/brainy.client.js
|
||||
// server/utils/brain.js (server-only — Nitro never bundles this into the client)
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'opfs' }
|
||||
})
|
||||
let brainPromise
|
||||
|
||||
await brain.init()
|
||||
|
||||
return {
|
||||
provide: {
|
||||
brain
|
||||
}
|
||||
export function getBrain() {
|
||||
if (!brainPromise) {
|
||||
brainPromise = (async () => {
|
||||
// new Brainy() auto-detects filesystem persistence on Node
|
||||
const brain = new Brainy()
|
||||
await brain.init()
|
||||
return brain
|
||||
})()
|
||||
}
|
||||
return brainPromise
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// server/api/brain/search.post.js
|
||||
import { getBrain } from '../../utils/brain'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { query, options } = await readBody(event)
|
||||
const brain = await getBrain()
|
||||
return { results: await brain.find(query, options) }
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
// server/api/brain/add.post.js
|
||||
import { getBrain } from '../../utils/brain'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { data, type, metadata } = await readBody(event)
|
||||
const brain = await getBrain()
|
||||
return { id: await brain.add({ data, type, metadata }) }
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
// server/api/brain/stats.get.js
|
||||
import { getBrain } from '../../utils/brain'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const brain = await getBrain()
|
||||
return await brain.stats()
|
||||
})
|
||||
```
|
||||
|
||||
|
|
@ -637,26 +628,25 @@ export default defineNuxtPlugin(async () => {
|
|||
```javascript
|
||||
// composables/useBrainy.js
|
||||
export const useBrainy = () => {
|
||||
const { $brain } = useNuxtApp()
|
||||
|
||||
const search = async (query, options = {}) => {
|
||||
return await $brain.find(query, options)
|
||||
return (await $fetch('/api/brain/search', {
|
||||
method: 'POST',
|
||||
body: { query, options }
|
||||
})).results
|
||||
}
|
||||
|
||||
const add = async (data, type, metadata) => {
|
||||
return await $brain.add({ data, type, metadata })
|
||||
return (await $fetch('/api/brain/add', {
|
||||
method: 'POST',
|
||||
body: { data, type, metadata }
|
||||
})).id
|
||||
}
|
||||
|
||||
const stats = async () => {
|
||||
return await $brain.stats()
|
||||
return await $fetch('/api/brain/stats')
|
||||
}
|
||||
|
||||
return {
|
||||
brain: $brain,
|
||||
search,
|
||||
add,
|
||||
stats
|
||||
}
|
||||
return { search, add, stats }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -666,26 +656,20 @@ export const useBrainy = () => {
|
|||
<!-- pages/search.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<h1>AI Search</h1>
|
||||
<h1>Search</h1>
|
||||
|
||||
<div v-if="pending" class="loading">
|
||||
Initializing AI...
|
||||
</div>
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
<input
|
||||
v-model="query"
|
||||
@input="handleSearch"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<div v-if="searching" class="loading">Searching...</div>
|
||||
|
||||
<div v-if="searching" class="loading">Searching...</div>
|
||||
|
||||
<div class="results">
|
||||
<div v-for="result in results" :key="result.id" class="result">
|
||||
<h3>{{ result.data }}</h3>
|
||||
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
<div class="results">
|
||||
<div v-for="result in results" :key="result.id" class="result">
|
||||
<h3>{{ result.data }}</h3>
|
||||
<p>Score: {{ (result.score * 100).toFixed(1) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -697,12 +681,6 @@ const { search } = useBrainy()
|
|||
const query = ref('')
|
||||
const results = ref([])
|
||||
const searching = ref(false)
|
||||
const pending = ref(true)
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
pending.value = false
|
||||
})
|
||||
|
||||
const handleSearch = debounce(async () => {
|
||||
if (!query.value.trim()) {
|
||||
|
|
@ -722,82 +700,58 @@ const handleSearch = debounce(async () => {
|
|||
</script>
|
||||
```
|
||||
|
||||
### Nuxt Configuration
|
||||
|
||||
```javascript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
ssr: false, // Disable SSR for browser-only features
|
||||
|
||||
// Or use client-side hydration
|
||||
nitro: {
|
||||
experimental: {
|
||||
wasm: true
|
||||
}
|
||||
},
|
||||
|
||||
vite: {
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 🛠️ Advanced Patterns
|
||||
|
||||
### Global State Management with Pinia
|
||||
|
||||
The store caches results and stats client-side; all Brainy work happens behind the server endpoints.
|
||||
|
||||
```javascript
|
||||
// stores/brainy.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
|
||||
export const useBrainyStore = defineStore('brainy', () => {
|
||||
const brain = ref(null)
|
||||
const isReady = ref(false)
|
||||
const error = ref(null)
|
||||
const stats = ref(null)
|
||||
|
||||
const init = async (options = {}) => {
|
||||
const search = async (query, options = {}) => {
|
||||
error.value = null
|
||||
try {
|
||||
brain.value = new Brainy(options)
|
||||
await brain.value.init()
|
||||
isReady.value = true
|
||||
await loadStats()
|
||||
const res = await fetch('/api/brain/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, options })
|
||||
})
|
||||
return (await res.json()).results
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Brainy initialization failed:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const search = async (query, options = {}) => {
|
||||
if (!isReady.value) throw new Error('Brain not ready')
|
||||
return await brain.value.find(query, options)
|
||||
}
|
||||
|
||||
const add = async (data, type, metadata) => {
|
||||
if (!isReady.value) throw new Error('Brain not ready')
|
||||
const id = await brain.value.add({ data, type, metadata })
|
||||
const res = await fetch('/api/brain/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data, type, metadata })
|
||||
})
|
||||
const { id } = await res.json()
|
||||
await loadStats() // Refresh stats
|
||||
return id
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
if (!isReady.value) return
|
||||
try {
|
||||
stats.value = await brain.value.stats()
|
||||
const res = await fetch('/api/brain/stats')
|
||||
stats.value = await res.json()
|
||||
} catch (err) {
|
||||
console.error('Failed to load stats:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brain: readonly(brain),
|
||||
isReady: readonly(isReady),
|
||||
error: readonly(error),
|
||||
stats: readonly(stats),
|
||||
init,
|
||||
search,
|
||||
add,
|
||||
loadStats
|
||||
|
|
@ -867,7 +821,7 @@ const showResults = ref(false)
|
|||
const selectedIndex = ref(-1)
|
||||
|
||||
const search = async (searchQuery) => {
|
||||
if (!brainyStore.isReady || !searchQuery.trim()) {
|
||||
if (!searchQuery.trim()) {
|
||||
results.value = []
|
||||
return
|
||||
}
|
||||
|
|
@ -1166,21 +1120,23 @@ onMounted(() => {
|
|||
|
||||
### Component Testing with Vitest
|
||||
|
||||
The component talks to the server over `fetch`, so mock the endpoint, not Brainy itself.
|
||||
|
||||
```javascript
|
||||
// tests/components/Search.test.js
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import Search from '../src/components/Search.vue'
|
||||
|
||||
// Mock Brainy
|
||||
vi.mock('@soulcraft/brainy', () => ({
|
||||
Brainy: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
find: vi.fn().mockResolvedValue([
|
||||
{ id: '1', data: 'Test result', score: 0.9 }
|
||||
])
|
||||
}))
|
||||
}))
|
||||
// Mock the server endpoint
|
||||
beforeEach(() => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
results: [{ id: '1', data: 'Test result', score: 0.9 }]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Search Component', () => {
|
||||
let wrapper
|
||||
|
|
@ -1193,14 +1149,7 @@ describe('Search Component', () => {
|
|||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows loading state initially', () => {
|
||||
expect(wrapper.text()).toContain('Initializing AI')
|
||||
})
|
||||
|
||||
it('performs search when input changes', async () => {
|
||||
// Wait for brain to initialize
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('test query')
|
||||
await input.trigger('input')
|
||||
|
|
@ -1208,6 +1157,7 @@ describe('Search Component', () => {
|
|||
// Wait for debounced search
|
||||
await new Promise(resolve => setTimeout(resolve, 350))
|
||||
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('Test result')
|
||||
})
|
||||
})
|
||||
|
|
@ -1241,6 +1191,8 @@ test('search functionality works', async ({ page }) => {
|
|||
|
||||
### Bundle Optimization
|
||||
|
||||
Keep Brainy out of the client bundle — it belongs to the server build only. Mark it external for SSR so the bundler resolves it at runtime instead of inlining it.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
|
|
@ -1248,63 +1200,46 @@ import vue from '@vitejs/plugin-vue'
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['@soulcraft/brainy']
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'brainy': ['@soulcraft/brainy']
|
||||
}
|
||||
}
|
||||
}
|
||||
ssr: {
|
||||
external: ['@soulcraft/brainy']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Wrap the endpoint call with retry/backoff so transient network failures don't surface to the user.
|
||||
|
||||
```javascript
|
||||
// src/composables/useBrainyWithErrorHandling.js
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Brainy } from '@soulcraft/brainy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useBrainyWithErrorHandling() {
|
||||
const brain = ref(null)
|
||||
const isReady = ref(false)
|
||||
export function useBrainyWithErrorHandling(endpoint = '/api/brain') {
|
||||
const error = ref(null)
|
||||
const retryCount = ref(0)
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
brain.value = new Brainy()
|
||||
await brain.value.init()
|
||||
isReady.value = true
|
||||
error.value = null
|
||||
retryCount.value = 0
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Brainy initialization failed:', err)
|
||||
|
||||
// Retry logic
|
||||
if (retryCount.value < 3) {
|
||||
retryCount.value++
|
||||
setTimeout(init, 2000 * retryCount.value)
|
||||
const search = async (query, options = {}, maxRetries = 3) => {
|
||||
error.value = null
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const res = await fetch(`${endpoint}/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, options })
|
||||
})
|
||||
if (!res.ok) throw new Error(`Search failed: ${res.status}`)
|
||||
return (await res.json()).results
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
if (attempt === maxRetries) throw err
|
||||
// Exponential backoff before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 2000 * (attempt + 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
|
||||
return {
|
||||
brain: readonly(brain),
|
||||
isReady: readonly(isReady),
|
||||
error: readonly(error),
|
||||
retry: init
|
||||
search
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1315,6 +1250,13 @@ Here's a complete Vue 3 application structure:
|
|||
|
||||
```
|
||||
my-brainy-vue-app/
|
||||
├── server/ # Server-side (Node/Bun) — hosts Brainy
|
||||
│ ├── utils/
|
||||
│ │ └── brain.js # Shared Brainy instance (getBrain)
|
||||
│ └── api/brain/
|
||||
│ ├── search.post.js
|
||||
│ ├── add.post.js
|
||||
│ └── stats.get.js
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── Search.vue
|
||||
|
|
@ -1336,7 +1278,7 @@ my-brainy-vue-app/
|
|||
└── package.json
|
||||
```
|
||||
|
||||
This provides a complete, production-ready Vue.js application with comprehensive Brainy integration.
|
||||
This provides a complete, production-ready Vue.js application: Brainy runs on the server, and the client talks to it over HTTP.
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue