vocs 2.4.0 → 2.5.1

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.
Files changed (37) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/internal/retriever.d.ts +21 -2
  4. package/dist/internal/retriever.d.ts.map +1 -1
  5. package/dist/internal/retriever.js +152 -43
  6. package/dist/internal/retriever.js.map +1 -1
  7. package/dist/internal/search.d.ts.map +1 -1
  8. package/dist/internal/search.js +2 -1
  9. package/dist/internal/search.js.map +1 -1
  10. package/dist/internal/vector-store.d.ts +98 -9
  11. package/dist/internal/vector-store.d.ts.map +1 -1
  12. package/dist/internal/vector-store.js +234 -6
  13. package/dist/internal/vector-store.js.map +1 -1
  14. package/dist/internal/vite-plugins.d.ts.map +1 -1
  15. package/dist/internal/vite-plugins.js +12 -5
  16. package/dist/internal/vite-plugins.js.map +1 -1
  17. package/dist/waku/internal/middleware/ai-search-warmup.d.ts +16 -0
  18. package/dist/waku/internal/middleware/ai-search-warmup.d.ts.map +1 -0
  19. package/dist/waku/internal/middleware/ai-search-warmup.js +54 -0
  20. package/dist/waku/internal/middleware/ai-search-warmup.js.map +1 -0
  21. package/dist/waku/internal/middleware/index.d.ts +1 -0
  22. package/dist/waku/internal/middleware/index.d.ts.map +1 -1
  23. package/dist/waku/internal/middleware/index.js +1 -0
  24. package/dist/waku/internal/middleware/index.js.map +1 -1
  25. package/dist/waku/middleware.d.ts +1 -1
  26. package/package.json +1 -1
  27. package/src/cli.ts +20 -1
  28. package/src/internal/retriever.test.ts +177 -10
  29. package/src/internal/retriever.ts +177 -46
  30. package/src/internal/search.test.ts +0 -2
  31. package/src/internal/search.ts +2 -1
  32. package/src/internal/vector-store.test.ts +183 -1
  33. package/src/internal/vector-store.ts +368 -9
  34. package/src/internal/vite-plugins.ts +13 -5
  35. package/src/waku/internal/middleware/ai-search-warmup.test.ts +102 -0
  36. package/src/waku/internal/middleware/ai-search-warmup.ts +52 -0
  37. package/src/waku/internal/middleware/index.ts +1 -0
@@ -0,0 +1,102 @@
1
+ import { Hono } from 'hono'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ // Mock Config.resolve / Retriever.ensureServerIndex so we can observe the
5
+ // warm-up kick without a real vocs config or index build.
6
+ vi.mock('../../../internal/config.js', async () => {
7
+ const actual = await vi.importActual<typeof import('../../../internal/config.js')>(
8
+ '../../../internal/config.js',
9
+ )
10
+ return { ...actual, resolve: vi.fn() }
11
+ })
12
+ vi.mock('../../../internal/retriever.js', async () => {
13
+ const actual = await vi.importActual<typeof import('../../../internal/retriever.js')>(
14
+ '../../../internal/retriever.js',
15
+ )
16
+ return { ...actual, ensureServerIndex: vi.fn() }
17
+ })
18
+
19
+ import * as Config from '../../../internal/config.js'
20
+ import * as Retriever from '../../../internal/retriever.js'
21
+ import { aiSearchWarmup } from './ai-search-warmup.js'
22
+
23
+ const mockResolve = vi.mocked(Config.resolve)
24
+ const mockEnsure = vi.mocked(Retriever.ensureServerIndex)
25
+
26
+ afterEach(() => vi.resetAllMocks())
27
+
28
+ /** Creates a Hono app with the warmup middleware and a catch-all handler. */
29
+ function createApp() {
30
+ const app = new Hono()
31
+ app.use('*', aiSearchWarmup())
32
+ app.get('*', (c) => c.text('ok'))
33
+ return app
34
+ }
35
+
36
+ function localConfig(target: 'static' | 'remote' = 'static') {
37
+ // The middleware checks `_localRetriever` presence and the vector-store target.
38
+ return { _localRetriever: { vectorStore: { target } } } as unknown as Config.Config
39
+ }
40
+
41
+ /** Lets the fire-and-forget warm-up chain settle. */
42
+ function settle() {
43
+ return new Promise((resolve) => setTimeout(resolve, 10))
44
+ }
45
+
46
+ describe('aiSearchWarmup', () => {
47
+ it('kicks off the index build once, without blocking requests', async () => {
48
+ mockResolve.mockResolvedValue(localConfig())
49
+ mockEnsure.mockReturnValue({ status: 'ready', promise: Promise.resolve() } as never)
50
+
51
+ const app = createApp()
52
+ expect(await (await app.request('http://localhost/')).text()).toBe('ok')
53
+ await vi.waitFor(() => expect(mockEnsure).toHaveBeenCalledTimes(1))
54
+ expect(mockEnsure.mock.calls[0]?.[1]?.loadManifest).toBeTypeOf('function')
55
+
56
+ // Subsequent requests don't re-kick the warm-up.
57
+ await app.request('http://localhost/docs')
58
+ await settle()
59
+ expect(mockEnsure).toHaveBeenCalledTimes(1)
60
+ })
61
+
62
+ it('skips when no local retriever is configured', async () => {
63
+ mockResolve.mockResolvedValue({} as Config.Config)
64
+
65
+ const app = createApp()
66
+ expect((await app.request('http://localhost/')).status).toBe(200)
67
+ await settle()
68
+ expect(mockEnsure).not.toHaveBeenCalled()
69
+ })
70
+
71
+ it('skips for a remote vector store (no in-process index to warm)', async () => {
72
+ mockResolve.mockResolvedValue(localConfig('remote'))
73
+
74
+ const app = createApp()
75
+ expect((await app.request('http://localhost/')).status).toBe(200)
76
+ await settle()
77
+ expect(mockEnsure).not.toHaveBeenCalled()
78
+ })
79
+
80
+ it('skips in development (dev loads the index lazily)', async () => {
81
+ const prev = process.env['NODE_ENV']
82
+ process.env['NODE_ENV'] = 'development'
83
+ try {
84
+ const app = createApp()
85
+ expect((await app.request('http://localhost/')).status).toBe(200)
86
+ await settle()
87
+ expect(mockResolve).not.toHaveBeenCalled()
88
+ expect(mockEnsure).not.toHaveBeenCalled()
89
+ } finally {
90
+ process.env['NODE_ENV'] = prev
91
+ }
92
+ })
93
+
94
+ it('serves requests even when warm-up fails', async () => {
95
+ mockResolve.mockRejectedValue(new Error('no config'))
96
+
97
+ const app = createApp()
98
+ expect((await app.request('http://localhost/')).status).toBe(200)
99
+ await settle()
100
+ expect(mockEnsure).not.toHaveBeenCalled()
101
+ })
102
+ })
@@ -0,0 +1,52 @@
1
+ import type { MiddlewareHandler } from 'hono'
2
+
3
+ /**
4
+ * Warms the local AI search index on the first request of a server instance.
5
+ *
6
+ * Serverless instances build their in-process index lazily, so without
7
+ * warming, the first `/api/search` on each cold instance answers
8
+ * `indexing: true`. Kicking the build off on the first request (usually a
9
+ * page load, well before the user opens search) hides that latency.
10
+ *
11
+ * The build runs within request context (required on Workers-style runtimes,
12
+ * which forbid I/O at module scope) and never blocks the request. Modules are
13
+ * imported lazily so server boot stays lean.
14
+ */
15
+ export const aiSearchWarmup = (): MiddlewareHandler => {
16
+ let warmed = false
17
+ return (context, next) => {
18
+ if (!warmed) {
19
+ warmed = true
20
+ const build = warm()
21
+ // Keep the build alive past the response where supported (Workers).
22
+ try {
23
+ context.executionCtx?.waitUntil?.(build)
24
+ } catch {
25
+ // Hono throws when the runtime has no execution context (Node).
26
+ }
27
+ }
28
+ return next()
29
+ }
30
+ }
31
+
32
+ export default aiSearchWarmup
33
+
34
+ /** Kicks the index build. Best-effort: `/api/search` surfaces real errors. */
35
+ async function warm(): Promise<void> {
36
+ try {
37
+ // Dev never builds the index (see `resolveServerIndex`); load it lazily.
38
+ if (process.env['NODE_ENV'] === 'development') return
39
+ const [Config, Retriever, { loadAiSearchManifest }] = await Promise.all([
40
+ import('../../../internal/config.js'),
41
+ import('../../../internal/retriever.js'),
42
+ import('../ai-search.js'),
43
+ ])
44
+ const config = await Config.resolve({ server: true })
45
+ if (!config._localRetriever) return
46
+ // Remote vector store: no in-process index to warm (queried in the database).
47
+ if (config._localRetriever.vectorStore.target === 'remote') return
48
+ await Retriever.ensureServerIndex(config, { loadManifest: loadAiSearchManifest }).promise
49
+ } catch {
50
+ // `ensureServerIndex` already logs build failures.
51
+ }
52
+ }
@@ -1,3 +1,4 @@
1
+ export { default as aiSearchWarmup } from './ai-search-warmup.js'
1
2
  export { default as mdRouter } from './md-router.js'
2
3
  export { default as redirects } from './redirects.js'
3
4
  export { default as trailingSlash } from './trailing-slash.js'