@remit/search-service 0.0.7 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/search-service",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "scripts": {
34
34
  "test:typecheck": "tsgo --noEmit",
35
- "test:run": "node --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=75 --test 'src/**/*.test.ts'",
35
+ "test:run": "node --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=82 --test 'src/**/*.test.ts'",
36
36
  "test:integ": "RUN_INTEG_TESTS=1 node --env-file=../../localhost-test-unit.env --import tsx --test 'src/**/*.integ.test.ts'",
37
37
  "test": "npm run test:typecheck && npm run test:run"
38
38
  },
@@ -1,6 +1,24 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
- import { LocalEmbeddingService } from "./embeddings.js";
3
+ import type { pipeline as PipelineFn } from "@huggingface/transformers";
4
+ import {
5
+ EmbeddingModelUnavailableError,
6
+ LocalEmbeddingService,
7
+ } from "./embeddings.js";
8
+
9
+ // Exercises the model-load path without the network: the pipeline factory
10
+ // rejects the way a failed `from_pretrained` (HuggingFace fetch failed) does.
11
+ class FailingLoadService extends LocalEmbeddingService {
12
+ importCount = 0;
13
+ protected importTransformers(): Promise<{ pipeline: typeof PipelineFn }> {
14
+ this.importCount++;
15
+ const pipeline = (() =>
16
+ Promise.reject(
17
+ new TypeError("fetch failed"),
18
+ )) as unknown as typeof PipelineFn;
19
+ return Promise.resolve({ pipeline });
20
+ }
21
+ }
4
22
 
5
23
  describe("LocalEmbeddingService", () => {
6
24
  it("keeps the historical embeddingId when dtype is unset", () => {
@@ -25,4 +43,26 @@ describe("LocalEmbeddingService", () => {
25
43
  "local:Xenova/paraphrase-multilingual-MiniLM-L12-v2:q8@384",
26
44
  );
27
45
  });
46
+
47
+ it("raises a typed EmbeddingModelUnavailableError when the model cannot be loaded", async () => {
48
+ const service = new FailingLoadService({ dimensions: 8 });
49
+ await assert.rejects(
50
+ () => service.embed(["hello"]),
51
+ (error: unknown) => {
52
+ assert.ok(error instanceof EmbeddingModelUnavailableError);
53
+ assert.equal(
54
+ (error as { code?: string }).code,
55
+ "ERR_EMBEDDING_MODEL_UNAVAILABLE",
56
+ );
57
+ return true;
58
+ },
59
+ );
60
+ });
61
+
62
+ it("clears the memoized pipeline on a load failure so a later call retries", async () => {
63
+ const service = new FailingLoadService({ dimensions: 8 });
64
+ await assert.rejects(() => service.embed(["hello"]));
65
+ await assert.rejects(() => service.embed(["hello"]));
66
+ assert.equal(service.importCount, 2);
67
+ });
28
68
  });
package/src/embeddings.ts CHANGED
@@ -91,6 +91,24 @@ export interface LocalEmbeddingConfig {
91
91
  const DEFAULT_LOCAL_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
92
92
  const DEFAULT_LOCAL_DIMENSIONS = 384;
93
93
 
94
+ /**
95
+ * The local embedder could not load its model. Raised when Transformers.js fails
96
+ * to resolve the model files — the common cause is `from_pretrained` lazily
97
+ * fetching the weights from HuggingFace and the network fetch failing
98
+ * (`TypeError: fetch failed`), which otherwise surfaces as an opaque 500.
99
+ *
100
+ * The `code` is the contract consumers match on (mirroring Node's own error
101
+ * codes) to degrade the semantic path to empty results instead of crashing,
102
+ * without importing this class or string-matching undici internals.
103
+ */
104
+ export class EmbeddingModelUnavailableError extends Error {
105
+ readonly code = "ERR_EMBEDDING_MODEL_UNAVAILABLE";
106
+ constructor(modelId: string, options?: { cause?: unknown }) {
107
+ super(`Embedding model "${modelId}" could not be loaded`, options);
108
+ this.name = "EmbeddingModelUnavailableError";
109
+ }
110
+ }
111
+
94
112
  type TransformersModule = { pipeline: typeof PipelineFn };
95
113
 
96
114
  /**
@@ -122,15 +140,34 @@ export class LocalEmbeddingService implements EmbeddingService {
122
140
 
123
141
  private getPipeline = async (): Promise<FeatureExtractionPipeline> => {
124
142
  if (this.pipelinePromise) return this.pipelinePromise;
125
- this.pipelinePromise = (async () => {
126
- const { pipeline } = await runtimeImport<TransformersModule>(
127
- "@huggingface/transformers",
128
- );
129
- return pipeline("feature-extraction", this.modelId, {
143
+ const promise = this.loadPipeline();
144
+ this.pipelinePromise = promise;
145
+ try {
146
+ return await promise;
147
+ } catch (error) {
148
+ // A failed load (module absent, or the model could not be fetched) must
149
+ // not poison the instance: clear the memo so a later call retries rather
150
+ // than returning the same rejected promise for the process lifetime.
151
+ if (this.pipelinePromise === promise) this.pipelinePromise = null;
152
+ throw error;
153
+ }
154
+ };
155
+
156
+ // Import boundary kept overridable so a load failure can be exercised without
157
+ // the network; the default resolves Transformers.js lazily via runtimeImport.
158
+ protected importTransformers(): Promise<TransformersModule> {
159
+ return runtimeImport<TransformersModule>("@huggingface/transformers");
160
+ }
161
+
162
+ private loadPipeline = async (): Promise<FeatureExtractionPipeline> => {
163
+ const { pipeline } = await this.importTransformers();
164
+ try {
165
+ return await pipeline("feature-extraction", this.modelId, {
130
166
  dtype: this.dtype,
131
167
  });
132
- })();
133
- return this.pipelinePromise;
168
+ } catch (error) {
169
+ throw new EmbeddingModelUnavailableError(this.modelId, { cause: error });
170
+ }
134
171
  };
135
172
 
136
173
  embed = async (texts: string[]): Promise<number[][]> => {
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export {
36
36
  createLocalEmbeddingService,
37
37
  type DeterministicEmbeddingConfig,
38
38
  DeterministicEmbeddingService,
39
+ EmbeddingModelUnavailableError,
39
40
  type EmbeddingService,
40
41
  type LocalEmbeddingConfig,
41
42
  LocalEmbeddingService,