@van1s1mys/ai-router 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # @van1s1mys/ai-router
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@van1s1mys/ai-router)](https://www.npmjs.com/package/@van1s1mys/ai-router)
4
+ [![GitHub](https://img.shields.io/github/stars/IvanMalkS/ai-router)](https://github.com/IvanMalkS/ai-router)
5
+
3
6
  Semantic search routing for SPAs — find the best route by meaning, not keywords.
4
7
 
8
+ [Documentation](https://ivanmalks.github.io/ai-router/) | [Live Demo](https://ai-router-search.vercel.app) | [npm](https://www.npmjs.com/package/@van1s1mys/ai-router) | [GitHub](https://github.com/IvanMalkS/ai-router)
9
+
5
10
  Runs a HuggingFace embedding model inside a **Web Worker** and uses [Orama](https://orama.com) hybrid (text + vector) search to match user queries by semantic similarity.
6
11
 
7
12
  ## Install
@@ -32,6 +37,31 @@ const result = await router.search('how to reach support?');
32
37
  router.destroy(); // cleanup when done
33
38
  ```
34
39
 
40
+ ## Progressive model loading
41
+
42
+ Start with a fast model, upgrade to a better one in the background:
43
+
44
+ ```ts
45
+ const router = new SmartRouter({
46
+ routes,
47
+ model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'],
48
+ onModelUpgrade: (modelId) => console.log(`Upgraded to ${modelId}`),
49
+ });
50
+
51
+ await router.ready; // first model ready — search works immediately
52
+ ```
53
+
54
+ ## Instance caching & preloading
55
+
56
+ ```ts
57
+ // Pre-warm at page load
58
+ SmartRouter.preload({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
59
+
60
+ // Later — returns cached instance, no re-download
61
+ const router = SmartRouter.create({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
62
+ await router.ready; // instant if preload finished
63
+ ```
64
+
35
65
  ## Multilingual support
36
66
 
37
67
  The default model (`Xenova/all-MiniLM-L6-v2`) works best for English. For other languages, use the multilingual model:
@@ -50,12 +80,21 @@ const router = new SmartRouter({
50
80
  | Option | Type | Default | Description |
51
81
  |---|---|---|---|
52
82
  | `routes` | `RouteConfig[]` | required | Routes to index |
53
- | `model` | `string` | `"Xenova/all-MiniLM-L6-v2"` | HuggingFace model ID (384-dim) |
83
+ | `model` | `string \| string[]` | `"Xenova/all-MiniLM-L6-v2"` | Model ID or ordered array for progressive loading |
54
84
  | `threshold` | `number` | `0.5` | Minimum similarity score (0-1) |
85
+ | `onModelUpgrade` | `(modelId: string) => void` | — | Called when the router switches to the next model |
86
+
87
+ ### `SmartRouter.create(options): SmartRouter`
88
+
89
+ Returns a cached instance for the given model config. Safe to call on every component mount.
90
+
91
+ ### `SmartRouter.preload(options): SmartRouter`
92
+
93
+ Same as `create()`, but intended to be called at page load to pre-warm the model.
55
94
 
56
95
  ### `router.ready: Promise<void>`
57
96
 
58
- Resolves when the model is loaded and routes are indexed. Resolves immediately during SSR.
97
+ Resolves when the first model is loaded and routes are indexed. Resolves immediately during SSR.
59
98
 
60
99
  ### `router.search(query): Promise<SearchResult | null>`
61
100
 
@@ -63,7 +102,7 @@ Returns `{ path, score }` or `null` if no route meets the threshold. Returns `nu
63
102
 
64
103
  ### `router.destroy()`
65
104
 
66
- Terminates the worker and cleans up resources. Safe to call multiple times.
105
+ Terminates the worker, cleans up resources, and removes from cache. Safe to call multiple times.
67
106
 
68
107
  ## SSR
69
108
 
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ function createBlobWorker() {
36
36
  URL.revokeObjectURL(url);
37
37
  return worker;
38
38
  }
39
- var SmartRouter = class {
39
+ var _SmartRouter = class _SmartRouter {
40
40
  /**
41
41
  * Creates a new SmartRouter instance.
42
42
  *
@@ -52,6 +52,7 @@ var SmartRouter = class {
52
52
  this.worker = null;
53
53
  this.pendingSearches = /* @__PURE__ */ new Map();
54
54
  this.destroyed = false;
55
+ this._cacheKey = null;
55
56
  this.ssr = !isBrowser;
56
57
  this.readyPromise = new Promise((resolve, reject) => {
57
58
  this.resolveReady = resolve;
@@ -64,6 +65,63 @@ var SmartRouter = class {
64
65
  this.onModelUpgrade = options.onModelUpgrade;
65
66
  this.initWorker(options);
66
67
  }
68
+ /**
69
+ * Returns a cached SmartRouter instance for the given options.
70
+ * If an instance with the same model configuration already exists and
71
+ * hasn't been destroyed, it is returned immediately — no new worker
72
+ * is spawned and no model is re-downloaded.
73
+ *
74
+ * Use this instead of `new SmartRouter()` when the router may be
75
+ * created multiple times (e.g. React component mounts/unmounts).
76
+ *
77
+ * @param options - Routes to index, model ID, and similarity threshold.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Safe to call on every mount — returns the same instance
82
+ * const router = SmartRouter.create({ routes, model: 'Xenova/all-MiniLM-L6-v2' });
83
+ * await router.ready;
84
+ * ```
85
+ */
86
+ static create(options) {
87
+ const key = _SmartRouter.cacheKey(options);
88
+ const cached = _SmartRouter.cache.get(key);
89
+ if (cached && !cached.destroyed) return cached;
90
+ const instance = new _SmartRouter(options);
91
+ instance._cacheKey = key;
92
+ _SmartRouter.cache.set(key, instance);
93
+ return instance;
94
+ }
95
+ /**
96
+ * Pre-downloads the model(s) and indexes routes in the background
97
+ * so that a later {@link create} call returns an already-ready instance.
98
+ *
99
+ * Safe to call at page load — runs in a Web Worker and does not
100
+ * block the main thread. No-op on the server (SSR).
101
+ * Calling it multiple times with the same options is a no-op.
102
+ *
103
+ * @param options - Routes to index, model ID, and similarity threshold.
104
+ * @returns The pre-warming SmartRouter instance (await `.ready` if needed).
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // At page load — start downloading the model immediately
109
+ * SmartRouter.preload({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
110
+ *
111
+ * // Later, when the user opens search — instant, model is already loaded
112
+ * const router = SmartRouter.create({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
113
+ * await router.ready; // resolves immediately if preload finished
114
+ * ```
115
+ */
116
+ static preload(options) {
117
+ return _SmartRouter.create(options);
118
+ }
119
+ /** @internal Generates a stable cache key from options. */
120
+ static cacheKey(options) {
121
+ return JSON.stringify(
122
+ Array.isArray(options.model) ? options.model : [options.model || "Xenova/all-MiniLM-L6-v2"]
123
+ );
124
+ }
67
125
  /**
68
126
  * Promise that resolves when the model is loaded and routes are indexed.
69
127
  * Rejects if initialization fails (e.g. network error, invalid model).
@@ -195,6 +253,9 @@ var SmartRouter = class {
195
253
  destroy() {
196
254
  if (this.destroyed) return;
197
255
  this.destroyed = true;
256
+ if (this._cacheKey) {
257
+ _SmartRouter.cache.delete(this._cacheKey);
258
+ }
198
259
  this.rejectAllPending(new Error("SmartRouter destroyed"));
199
260
  if (this.worker) {
200
261
  try {
@@ -213,6 +274,8 @@ var SmartRouter = class {
213
274
  this.pendingSearches.clear();
214
275
  }
215
276
  };
277
+ _SmartRouter.cache = /* @__PURE__ */ new Map();
278
+ var SmartRouter = _SmartRouter;
216
279
  // Annotate the CommonJS export names for ESM import in node:
217
280
  0 && (module.exports = {
218
281
  SmartRouter
package/dist/index.d.cts CHANGED
@@ -119,6 +119,7 @@ interface SearchResult {
119
119
  * ```
120
120
  */
121
121
  declare class SmartRouter {
122
+ private static cache;
122
123
  private worker;
123
124
  private readyPromise;
124
125
  private resolveReady;
@@ -127,6 +128,50 @@ declare class SmartRouter {
127
128
  private destroyed;
128
129
  private readonly ssr;
129
130
  private onModelUpgrade?;
131
+ private _cacheKey;
132
+ /**
133
+ * Returns a cached SmartRouter instance for the given options.
134
+ * If an instance with the same model configuration already exists and
135
+ * hasn't been destroyed, it is returned immediately — no new worker
136
+ * is spawned and no model is re-downloaded.
137
+ *
138
+ * Use this instead of `new SmartRouter()` when the router may be
139
+ * created multiple times (e.g. React component mounts/unmounts).
140
+ *
141
+ * @param options - Routes to index, model ID, and similarity threshold.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Safe to call on every mount — returns the same instance
146
+ * const router = SmartRouter.create({ routes, model: 'Xenova/all-MiniLM-L6-v2' });
147
+ * await router.ready;
148
+ * ```
149
+ */
150
+ static create(options: RouteOptions): SmartRouter;
151
+ /**
152
+ * Pre-downloads the model(s) and indexes routes in the background
153
+ * so that a later {@link create} call returns an already-ready instance.
154
+ *
155
+ * Safe to call at page load — runs in a Web Worker and does not
156
+ * block the main thread. No-op on the server (SSR).
157
+ * Calling it multiple times with the same options is a no-op.
158
+ *
159
+ * @param options - Routes to index, model ID, and similarity threshold.
160
+ * @returns The pre-warming SmartRouter instance (await `.ready` if needed).
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * // At page load — start downloading the model immediately
165
+ * SmartRouter.preload({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
166
+ *
167
+ * // Later, when the user opens search — instant, model is already loaded
168
+ * const router = SmartRouter.create({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
169
+ * await router.ready; // resolves immediately if preload finished
170
+ * ```
171
+ */
172
+ static preload(options: RouteOptions): SmartRouter;
173
+ /** @internal Generates a stable cache key from options. */
174
+ private static cacheKey;
130
175
  /**
131
176
  * Creates a new SmartRouter instance.
132
177
  *
package/dist/index.d.ts CHANGED
@@ -119,6 +119,7 @@ interface SearchResult {
119
119
  * ```
120
120
  */
121
121
  declare class SmartRouter {
122
+ private static cache;
122
123
  private worker;
123
124
  private readyPromise;
124
125
  private resolveReady;
@@ -127,6 +128,50 @@ declare class SmartRouter {
127
128
  private destroyed;
128
129
  private readonly ssr;
129
130
  private onModelUpgrade?;
131
+ private _cacheKey;
132
+ /**
133
+ * Returns a cached SmartRouter instance for the given options.
134
+ * If an instance with the same model configuration already exists and
135
+ * hasn't been destroyed, it is returned immediately — no new worker
136
+ * is spawned and no model is re-downloaded.
137
+ *
138
+ * Use this instead of `new SmartRouter()` when the router may be
139
+ * created multiple times (e.g. React component mounts/unmounts).
140
+ *
141
+ * @param options - Routes to index, model ID, and similarity threshold.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Safe to call on every mount — returns the same instance
146
+ * const router = SmartRouter.create({ routes, model: 'Xenova/all-MiniLM-L6-v2' });
147
+ * await router.ready;
148
+ * ```
149
+ */
150
+ static create(options: RouteOptions): SmartRouter;
151
+ /**
152
+ * Pre-downloads the model(s) and indexes routes in the background
153
+ * so that a later {@link create} call returns an already-ready instance.
154
+ *
155
+ * Safe to call at page load — runs in a Web Worker and does not
156
+ * block the main thread. No-op on the server (SSR).
157
+ * Calling it multiple times with the same options is a no-op.
158
+ *
159
+ * @param options - Routes to index, model ID, and similarity threshold.
160
+ * @returns The pre-warming SmartRouter instance (await `.ready` if needed).
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * // At page load — start downloading the model immediately
165
+ * SmartRouter.preload({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
166
+ *
167
+ * // Later, when the user opens search — instant, model is already loaded
168
+ * const router = SmartRouter.create({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
169
+ * await router.ready; // resolves immediately if preload finished
170
+ * ```
171
+ */
172
+ static preload(options: RouteOptions): SmartRouter;
173
+ /** @internal Generates a stable cache key from options. */
174
+ private static cacheKey;
130
175
  /**
131
176
  * Creates a new SmartRouter instance.
132
177
  *
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ function createBlobWorker() {
10
10
  URL.revokeObjectURL(url);
11
11
  return worker;
12
12
  }
13
- var SmartRouter = class {
13
+ var _SmartRouter = class _SmartRouter {
14
14
  /**
15
15
  * Creates a new SmartRouter instance.
16
16
  *
@@ -26,6 +26,7 @@ var SmartRouter = class {
26
26
  this.worker = null;
27
27
  this.pendingSearches = /* @__PURE__ */ new Map();
28
28
  this.destroyed = false;
29
+ this._cacheKey = null;
29
30
  this.ssr = !isBrowser;
30
31
  this.readyPromise = new Promise((resolve, reject) => {
31
32
  this.resolveReady = resolve;
@@ -38,6 +39,63 @@ var SmartRouter = class {
38
39
  this.onModelUpgrade = options.onModelUpgrade;
39
40
  this.initWorker(options);
40
41
  }
42
+ /**
43
+ * Returns a cached SmartRouter instance for the given options.
44
+ * If an instance with the same model configuration already exists and
45
+ * hasn't been destroyed, it is returned immediately — no new worker
46
+ * is spawned and no model is re-downloaded.
47
+ *
48
+ * Use this instead of `new SmartRouter()` when the router may be
49
+ * created multiple times (e.g. React component mounts/unmounts).
50
+ *
51
+ * @param options - Routes to index, model ID, and similarity threshold.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // Safe to call on every mount — returns the same instance
56
+ * const router = SmartRouter.create({ routes, model: 'Xenova/all-MiniLM-L6-v2' });
57
+ * await router.ready;
58
+ * ```
59
+ */
60
+ static create(options) {
61
+ const key = _SmartRouter.cacheKey(options);
62
+ const cached = _SmartRouter.cache.get(key);
63
+ if (cached && !cached.destroyed) return cached;
64
+ const instance = new _SmartRouter(options);
65
+ instance._cacheKey = key;
66
+ _SmartRouter.cache.set(key, instance);
67
+ return instance;
68
+ }
69
+ /**
70
+ * Pre-downloads the model(s) and indexes routes in the background
71
+ * so that a later {@link create} call returns an already-ready instance.
72
+ *
73
+ * Safe to call at page load — runs in a Web Worker and does not
74
+ * block the main thread. No-op on the server (SSR).
75
+ * Calling it multiple times with the same options is a no-op.
76
+ *
77
+ * @param options - Routes to index, model ID, and similarity threshold.
78
+ * @returns The pre-warming SmartRouter instance (await `.ready` if needed).
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * // At page load — start downloading the model immediately
83
+ * SmartRouter.preload({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
84
+ *
85
+ * // Later, when the user opens search — instant, model is already loaded
86
+ * const router = SmartRouter.create({ routes, model: ['Xenova/all-MiniLM-L6-v2', 'Xenova/multilingual-e5-small'] });
87
+ * await router.ready; // resolves immediately if preload finished
88
+ * ```
89
+ */
90
+ static preload(options) {
91
+ return _SmartRouter.create(options);
92
+ }
93
+ /** @internal Generates a stable cache key from options. */
94
+ static cacheKey(options) {
95
+ return JSON.stringify(
96
+ Array.isArray(options.model) ? options.model : [options.model || "Xenova/all-MiniLM-L6-v2"]
97
+ );
98
+ }
41
99
  /**
42
100
  * Promise that resolves when the model is loaded and routes are indexed.
43
101
  * Rejects if initialization fails (e.g. network error, invalid model).
@@ -169,6 +227,9 @@ var SmartRouter = class {
169
227
  destroy() {
170
228
  if (this.destroyed) return;
171
229
  this.destroyed = true;
230
+ if (this._cacheKey) {
231
+ _SmartRouter.cache.delete(this._cacheKey);
232
+ }
172
233
  this.rejectAllPending(new Error("SmartRouter destroyed"));
173
234
  if (this.worker) {
174
235
  try {
@@ -187,6 +248,8 @@ var SmartRouter = class {
187
248
  this.pendingSearches.clear();
188
249
  }
189
250
  };
251
+ _SmartRouter.cache = /* @__PURE__ */ new Map();
252
+ var SmartRouter = _SmartRouter;
190
253
  export {
191
254
  SmartRouter
192
255
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@van1s1mys/ai-router",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Semantic search routing using AI embeddings — find the best route by meaning, not keywords",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -34,6 +34,15 @@
34
34
  "spa",
35
35
  "navigation"
36
36
  ],
37
+ "homepage": "https://ivanmalks.github.io/ai-router/",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/IvanMalkS/ai-router.git",
41
+ "directory": "packages/core"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/IvanMalkS/ai-router/issues"
45
+ },
37
46
  "license": "MIT",
38
47
  "devDependencies": {
39
48
  "esbuild": "^0.27.0"