@untangle-code/embeddings-ollama 0.0.0-stage → 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/LICENSE +661 -0
- package/NOTICE +90 -0
- package/README.md +187 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/backoff.d.ts +43 -0
- package/dist/backoff.d.ts.map +1 -0
- package/dist/backoff.js +38 -0
- package/dist/backoff.js.map +1 -0
- package/dist/cache.d.ts +17 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +17 -0
- package/dist/cache.js.map +1 -0
- package/dist/concurrency.d.ts +11 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +31 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/endpoint.d.ts +16 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +32 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/http.d.ts +54 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +136 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/inflight.d.ts +49 -0
- package/dist/inflight.d.ts.map +1 -0
- package/dist/inflight.js +87 -0
- package/dist/inflight.js.map +1 -0
- package/dist/ollama-api.d.ts +45 -0
- package/dist/ollama-api.d.ts.map +1 -0
- package/dist/ollama-api.js +40 -0
- package/dist/ollama-api.js.map +1 -0
- package/dist/provider.d.ts +111 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +374 -0
- package/dist/provider.js.map +1 -0
- package/dist/readiness.d.ts +31 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +105 -0
- package/dist/readiness.js.map +1 -0
- package/dist/sleep.d.ts +8 -0
- package/dist/sleep.d.ts.map +1 -0
- package/dist/sleep.js +20 -0
- package/dist/sleep.js.map +1 -0
- package/dist/truncation.d.ts +30 -0
- package/dist/truncation.d.ts.map +1 -0
- package/dist/truncation.js +39 -0
- package/dist/truncation.js.map +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +16 -0
- package/dist/validation.js.map +1 -0
- package/package.json +62 -3
- package/src/backoff.ts +73 -0
- package/src/cache.ts +30 -0
- package/src/concurrency.ts +37 -0
- package/src/endpoint.ts +31 -0
- package/src/http.ts +168 -0
- package/src/index.ts +21 -0
- package/src/inflight.ts +102 -0
- package/src/ollama-api.ts +93 -0
- package/src/provider.ts +477 -0
- package/src/readiness.ts +203 -0
- package/src/sleep.ts +27 -0
- package/src/truncation.ts +41 -0
- package/src/types.ts +132 -0
- package/src/validation.ts +23 -0
package/dist/http.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Ollama HTTP client on Node 22's built-in `fetch` (no HTTP client
|
|
3
|
+
* dependency, per the WP-10 task). Every request carries a timeout and
|
|
4
|
+
* respects an external `AbortSignal`; a bounded semaphore caps concurrently
|
|
5
|
+
* in-flight requests (spec.md §9.2 "Verbindungslimit").
|
|
6
|
+
*/
|
|
7
|
+
import { joinUrl } from "./endpoint.js";
|
|
8
|
+
/** Thrown by {@link OllamaHttpClient.requestJson} for any failed request. */
|
|
9
|
+
export class OllamaHttpError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
retryable;
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
14
|
+
this.name = "OllamaHttpError";
|
|
15
|
+
this.status = options.status;
|
|
16
|
+
this.retryable = options.retryable;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Thrown when a request is aborted by the CALLER's own signal (never retried). */
|
|
20
|
+
export class OllamaRequestCancelledError extends Error {
|
|
21
|
+
constructor(cause) {
|
|
22
|
+
super("Ollama request cancelled", cause === undefined ? undefined : { cause });
|
|
23
|
+
this.name = "OllamaRequestCancelledError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True for anything that represents the CALLER's own cancellation rather
|
|
28
|
+
* than a transport failure: an {@link OllamaRequestCancelledError}, a raw
|
|
29
|
+
* `AbortError` (e.g. from `backoff.ts`'s own `signal.throwIfAborted()`
|
|
30
|
+
* pre-check, which runs before this client gets a chance to wrap it), or
|
|
31
|
+
* simply `signal.aborted` already being true by the time the error is
|
|
32
|
+
* observed. Used everywhere this adapter must tell "the caller cancelled"
|
|
33
|
+
* apart from "the endpoint is unreachable" (rule 9: cancellation surfaces as
|
|
34
|
+
* `JOB_CANCELLED`, never as a swallowed/misclassified failure).
|
|
35
|
+
*/
|
|
36
|
+
export function isRequestCancellation(error, signal) {
|
|
37
|
+
return (error instanceof OllamaRequestCancelledError ||
|
|
38
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
39
|
+
signal?.aborted === true);
|
|
40
|
+
}
|
|
41
|
+
function isRetryableStatus(status) {
|
|
42
|
+
return status === 429 || status >= 500;
|
|
43
|
+
}
|
|
44
|
+
/** Counting semaphore bounding concurrent HTTP requests to one Ollama endpoint. */
|
|
45
|
+
class Semaphore {
|
|
46
|
+
available;
|
|
47
|
+
waiters = [];
|
|
48
|
+
constructor(limit) {
|
|
49
|
+
this.available = Math.max(1, limit);
|
|
50
|
+
}
|
|
51
|
+
async acquire() {
|
|
52
|
+
if (this.available > 0) {
|
|
53
|
+
this.available -= 1;
|
|
54
|
+
return () => this.release();
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
57
|
+
this.available -= 1;
|
|
58
|
+
return () => this.release();
|
|
59
|
+
}
|
|
60
|
+
release() {
|
|
61
|
+
this.available += 1;
|
|
62
|
+
const next = this.waiters.shift();
|
|
63
|
+
if (next) {
|
|
64
|
+
next();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class OllamaHttpClient {
|
|
69
|
+
semaphore;
|
|
70
|
+
fetchImpl;
|
|
71
|
+
requestTimeoutMs;
|
|
72
|
+
apiKeyEnv;
|
|
73
|
+
constructor(options) {
|
|
74
|
+
this.semaphore = new Semaphore(options.maxConnections);
|
|
75
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
76
|
+
this.requestTimeoutMs = options.requestTimeoutMs;
|
|
77
|
+
this.apiKeyEnv = options.apiKeyEnv;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Sends one JSON request and parses a JSON response. Never retries by
|
|
81
|
+
* itself — callers wrap this in `backoff.ts`'s `withRetry`.
|
|
82
|
+
*
|
|
83
|
+
* Throws {@link OllamaRequestCancelledError} when `signal` (the caller's
|
|
84
|
+
* own signal, not the internal timeout) is the reason for the abort, and
|
|
85
|
+
* {@link OllamaHttpError} for every other failure (network error, timeout,
|
|
86
|
+
* non-2xx status), with `retryable` set per spec.md §9.2's transient/
|
|
87
|
+
* permanent split.
|
|
88
|
+
*/
|
|
89
|
+
async requestJson(baseUrl, path, init, signal) {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
throw new OllamaRequestCancelledError(signal.reason);
|
|
92
|
+
}
|
|
93
|
+
const release = await this.semaphore.acquire();
|
|
94
|
+
try {
|
|
95
|
+
const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
|
|
96
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
97
|
+
const headers = { "content-type": "application/json" };
|
|
98
|
+
const token = this.apiKeyEnv ? process.env[this.apiKeyEnv] : undefined;
|
|
99
|
+
if (token) {
|
|
100
|
+
headers["authorization"] = `Bearer ${token}`;
|
|
101
|
+
}
|
|
102
|
+
let response;
|
|
103
|
+
try {
|
|
104
|
+
response = await this.fetchImpl(joinUrl(baseUrl, path), {
|
|
105
|
+
method: init.method,
|
|
106
|
+
headers,
|
|
107
|
+
signal: combinedSignal,
|
|
108
|
+
// Conditionally spread rather than `body: undefined` — RequestInit's
|
|
109
|
+
// `body` has no `| undefined` arm, so exactOptionalPropertyTypes
|
|
110
|
+
// forbids assigning it explicitly (a GET request has no body).
|
|
111
|
+
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (signal?.aborted) {
|
|
116
|
+
throw new OllamaRequestCancelledError(error);
|
|
117
|
+
}
|
|
118
|
+
// Network failure (connection refused, DNS, our own timeout) — transient.
|
|
119
|
+
throw new OllamaHttpError(`Ollama request to ${path} failed: ${String(error)}`, {
|
|
120
|
+
retryable: true,
|
|
121
|
+
cause: error,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const bodyText = await response.text().catch(() => "");
|
|
126
|
+
throw new OllamaHttpError(`Ollama request to ${path} returned HTTP ${response.status}: ${bodyText.slice(0, 500)}`, { status: response.status, retryable: isRetryableStatus(response.status) });
|
|
127
|
+
}
|
|
128
|
+
const text = await response.text();
|
|
129
|
+
return text.length === 0 ? undefined : JSON.parse(text);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
release();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @untangle-code/embeddings-ollama — `EmbeddingProvider` adapter for Ollama
|
|
3
|
+
* (spec.md §9, architecture.md §6.8). This module is the only public export
|
|
4
|
+
* of the package (rule 11); everything else (`http.ts`, `ollama-api.ts`,
|
|
5
|
+
* `readiness.ts`, `backoff.ts`, `concurrency.ts`, `truncation.ts`,
|
|
6
|
+
* `endpoint.ts`, `validation.ts`) is an internal implementation detail,
|
|
7
|
+
* imported directly only by this package's own co-located tests.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./provider.js";
|
|
10
|
+
export * from "./types.js";
|
|
11
|
+
export * from "./cache.js";
|
|
12
|
+
export * from "./sleep.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,cAAc,eAAe,CAAC;AAG9B,cAAc,YAAY,CAAC;AAG3B,cAAc,YAAY,CAAC;AAI3B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @untangle-code/embeddings-ollama — `EmbeddingProvider` adapter for Ollama
|
|
3
|
+
* (spec.md §9, architecture.md §6.8). This module is the only public export
|
|
4
|
+
* of the package (rule 11); everything else (`http.ts`, `ollama-api.ts`,
|
|
5
|
+
* `readiness.ts`, `backoff.ts`, `concurrency.ts`, `truncation.ts`,
|
|
6
|
+
* `endpoint.ts`, `validation.ts`) is an internal implementation detail,
|
|
7
|
+
* imported directly only by this package's own co-located tests.
|
|
8
|
+
*/
|
|
9
|
+
// The adapter itself.
|
|
10
|
+
export * from "./provider.js";
|
|
11
|
+
// Public configuration and port types.
|
|
12
|
+
export * from "./types.js";
|
|
13
|
+
// Default in-memory embedding cache (spec.md §9.4) — the port itself lives in types.ts.
|
|
14
|
+
export * from "./cache.js";
|
|
15
|
+
// Real (non-fake) sleep implementation, exported so callers assembling
|
|
16
|
+
// `OllamaEmbeddingProviderDeps` outside tests don't need to reimplement it.
|
|
17
|
+
export * from "./sleep.js";
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,sBAAsB;AACtB,cAAc,eAAe,CAAC;AAE9B,uCAAuC;AACvC,cAAc,YAAY,CAAC;AAE3B,wFAAwF;AACxF,cAAc,YAAY,CAAC;AAE3B,uEAAuE;AACvE,4EAA4E;AAC5E,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight / in-flight de-duplication primitives (spec.md §9.4, WP-78).
|
|
3
|
+
*
|
|
4
|
+
* Used to make sure a running async operation for a given key is REUSED by
|
|
5
|
+
* concurrent callers instead of started twice — both for embedding a
|
|
6
|
+
* not-yet-cached text (`provider.ts`'s `embedSlice`) and for the readiness
|
|
7
|
+
* sequence (`ensureReadyOnce`). Deliberately generic and cache-agnostic:
|
|
8
|
+
* these helpers only manage the "who is doing this right now" bookkeeping,
|
|
9
|
+
* never the cache or the network call itself.
|
|
10
|
+
*/
|
|
11
|
+
/** An externally-settleable promise: `resolve`/`reject` are exposed to the caller. */
|
|
12
|
+
export interface Deferred<T> {
|
|
13
|
+
promise: Promise<T>;
|
|
14
|
+
resolve: (value: T) => void;
|
|
15
|
+
reject: (reason: unknown) => void;
|
|
16
|
+
}
|
|
17
|
+
/** Creates a {@link Deferred}. The executor runs synchronously, so both fields are always assigned. */
|
|
18
|
+
export declare function createDeferred<T>(): Deferred<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Awaits `shared` — a promise potentially also being awaited by OTHER
|
|
21
|
+
* callers — but lets THIS caller's own `signal` reject early without
|
|
22
|
+
* affecting `shared` itself: the underlying operation keeps running (and
|
|
23
|
+
* other joined callers keep waiting on it) even after one waiter's own
|
|
24
|
+
* signal aborts (WP-78 requirement: one waiter's cancellation must not
|
|
25
|
+
* cancel the operation for everyone else).
|
|
26
|
+
*/
|
|
27
|
+
export declare function raceWithSignal<T>(shared: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Registry of in-flight operations keyed by an arbitrary string. `claim`
|
|
30
|
+
* MUST be called synchronously (no `await`) between checking for an
|
|
31
|
+
* existing entry and registering a new one — see call sites in
|
|
32
|
+
* `provider.ts` — so that two concurrent callers observing the same "no
|
|
33
|
+
* entry yet" state cannot both start the operation (the actual JS-level
|
|
34
|
+
* atomicity comes from there being no `await` in the claiming code, not
|
|
35
|
+
* from this class).
|
|
36
|
+
*/
|
|
37
|
+
export declare class InFlightRegistry<T> {
|
|
38
|
+
private readonly entries;
|
|
39
|
+
/** Returns the in-flight promise for `key`, if any is currently registered. */
|
|
40
|
+
get(key: string): Promise<T> | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Registers `promise` as the in-flight operation for `key`. The entry is
|
|
43
|
+
* removed automatically once `promise` settles — successfully or not — so
|
|
44
|
+
* a failed attempt frees the key immediately for a fresh retry (WP-78:
|
|
45
|
+
* "ein Fehlschlag den Eintrag wieder freigibt").
|
|
46
|
+
*/
|
|
47
|
+
register(key: string, promise: Promise<T>): void;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=inflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inflight.d.ts","sourceRoot":"","sources":["../src/inflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sFAAsF;AACtF,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAED,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAQ/C;AAMD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAuBjG;AAED;;;;;;;;GAQG;AACH,qBAAa,gBAAgB,CAAC,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IAEzD,+EAA+E;IACxE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS;IAI/C;;;;;OAKG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;CAYxD"}
|
package/dist/inflight.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight / in-flight de-duplication primitives (spec.md §9.4, WP-78).
|
|
3
|
+
*
|
|
4
|
+
* Used to make sure a running async operation for a given key is REUSED by
|
|
5
|
+
* concurrent callers instead of started twice — both for embedding a
|
|
6
|
+
* not-yet-cached text (`provider.ts`'s `embedSlice`) and for the readiness
|
|
7
|
+
* sequence (`ensureReadyOnce`). Deliberately generic and cache-agnostic:
|
|
8
|
+
* these helpers only manage the "who is doing this right now" bookkeeping,
|
|
9
|
+
* never the cache or the network call itself.
|
|
10
|
+
*/
|
|
11
|
+
/** Creates a {@link Deferred}. The executor runs synchronously, so both fields are always assigned. */
|
|
12
|
+
export function createDeferred() {
|
|
13
|
+
let resolve;
|
|
14
|
+
let reject;
|
|
15
|
+
const promise = new Promise((res, rej) => {
|
|
16
|
+
resolve = res;
|
|
17
|
+
reject = rej;
|
|
18
|
+
});
|
|
19
|
+
return { promise, resolve, reject };
|
|
20
|
+
}
|
|
21
|
+
function toAbortError(signal) {
|
|
22
|
+
return signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Awaits `shared` — a promise potentially also being awaited by OTHER
|
|
26
|
+
* callers — but lets THIS caller's own `signal` reject early without
|
|
27
|
+
* affecting `shared` itself: the underlying operation keeps running (and
|
|
28
|
+
* other joined callers keep waiting on it) even after one waiter's own
|
|
29
|
+
* signal aborts (WP-78 requirement: one waiter's cancellation must not
|
|
30
|
+
* cancel the operation for everyone else).
|
|
31
|
+
*/
|
|
32
|
+
export function raceWithSignal(shared, signal) {
|
|
33
|
+
if (!signal) {
|
|
34
|
+
return shared;
|
|
35
|
+
}
|
|
36
|
+
if (signal.aborted) {
|
|
37
|
+
return Promise.reject(toAbortError(signal));
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
reject(toAbortError(signal));
|
|
42
|
+
};
|
|
43
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
shared.then((value) => {
|
|
45
|
+
signal.removeEventListener("abort", onAbort);
|
|
46
|
+
resolve(value);
|
|
47
|
+
}, (error) => {
|
|
48
|
+
signal.removeEventListener("abort", onAbort);
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Registry of in-flight operations keyed by an arbitrary string. `claim`
|
|
55
|
+
* MUST be called synchronously (no `await`) between checking for an
|
|
56
|
+
* existing entry and registering a new one — see call sites in
|
|
57
|
+
* `provider.ts` — so that two concurrent callers observing the same "no
|
|
58
|
+
* entry yet" state cannot both start the operation (the actual JS-level
|
|
59
|
+
* atomicity comes from there being no `await` in the claiming code, not
|
|
60
|
+
* from this class).
|
|
61
|
+
*/
|
|
62
|
+
export class InFlightRegistry {
|
|
63
|
+
entries = new Map();
|
|
64
|
+
/** Returns the in-flight promise for `key`, if any is currently registered. */
|
|
65
|
+
get(key) {
|
|
66
|
+
return this.entries.get(key);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Registers `promise` as the in-flight operation for `key`. The entry is
|
|
70
|
+
* removed automatically once `promise` settles — successfully or not — so
|
|
71
|
+
* a failed attempt frees the key immediately for a fresh retry (WP-78:
|
|
72
|
+
* "ein Fehlschlag den Eintrag wieder freigibt").
|
|
73
|
+
*/
|
|
74
|
+
register(key, promise) {
|
|
75
|
+
this.entries.set(key, promise);
|
|
76
|
+
// Void: this cleanup must never itself become an unhandled rejection —
|
|
77
|
+
// the caller is responsible for handling/propagating `promise`'s outcome.
|
|
78
|
+
void promise
|
|
79
|
+
.catch(() => undefined)
|
|
80
|
+
.finally(() => {
|
|
81
|
+
if (this.entries.get(key) === promise) {
|
|
82
|
+
this.entries.delete(key);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=inflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inflight.js","sourceRoot":"","sources":["../src/inflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,uGAAuG;AACvG,MAAM,UAAU,cAAc;IAC5B,IAAI,OAA4B,CAAC;IACjC,IAAI,MAAkC,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC1C,OAAO,GAAG,GAAG,CAAC;QACd,MAAM,GAAG,GAAG,CAAC;IACf,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,YAAY,CAAC,MAAmB;IACvC,OAAO,MAAM,CAAC,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AACpG,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAI,MAAkB,EAAE,MAA+B;IACnF,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC;QACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,MAAM,CAAC,IAAI,CACT,CAAC,KAAK,EAAE,EAAE;YACR,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IACV,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEzD,+EAA+E;IACxE,GAAG,CAAC,GAAW;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;OAKG;IACI,QAAQ,CAAC,GAAW,EAAE,OAAmB;QAC9C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC/B,uEAAuE;QACvE,0EAA0E;QAC1E,KAAK,OAAO;aACT,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;aACtB,OAAO,CAAC,GAAG,EAAE;YACZ,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC;gBACtC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC,CAAC;IACP,CAAC;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed slice of the Ollama HTTP API this adapter uses: `/api/tags` (model
|
|
3
|
+
* listing / reachability probe), `/api/pull` (managed-mode model fetch) and
|
|
4
|
+
* `/api/embed` (batch embedding). Shapes are the subset of each response
|
|
5
|
+
* this adapter reads — Ollama's actual responses carry more fields.
|
|
6
|
+
*/
|
|
7
|
+
import type { OllamaHttpClient } from "./http.js";
|
|
8
|
+
export interface OllamaTagV1 {
|
|
9
|
+
name: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface OllamaTagsResponseV1 {
|
|
13
|
+
models: OllamaTagV1[];
|
|
14
|
+
}
|
|
15
|
+
export interface OllamaPullResponseV1 {
|
|
16
|
+
status: string;
|
|
17
|
+
}
|
|
18
|
+
export interface OllamaEmbedResponseV1 {
|
|
19
|
+
model: string;
|
|
20
|
+
embeddings: number[][];
|
|
21
|
+
}
|
|
22
|
+
/** `GET /api/tags` — used both as the reachability probe and the model-availability check. */
|
|
23
|
+
export declare function listTags(client: OllamaHttpClient, baseUrl: string, signal?: AbortSignal): Promise<OllamaTagsResponseV1>;
|
|
24
|
+
/**
|
|
25
|
+
* True when `modelTags` (as returned by {@link listTags}) contains
|
|
26
|
+
* `modelName`. Ollama tag names carry an implicit `:latest` suffix when the
|
|
27
|
+
* configured model name has none, so `"nomic-embed-text"` must match a tag
|
|
28
|
+
* named `"nomic-embed-text:latest"`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function tagsContainModel(modelTags: OllamaTagV1[], modelName: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `POST /api/pull` with `stream: false` — Ollama blocks until the pull
|
|
33
|
+
* completes (or fails) and returns a single JSON response, which keeps this
|
|
34
|
+
* call symmetric with every other request this client makes (one request,
|
|
35
|
+
* one JSON response, no NDJSON stream parsing).
|
|
36
|
+
*/
|
|
37
|
+
export declare function pullModel(client: OllamaHttpClient, baseUrl: string, modelName: string, signal?: AbortSignal): Promise<OllamaPullResponseV1>;
|
|
38
|
+
/**
|
|
39
|
+
* `POST /api/embed`. `truncate: true` keeps Ollama's own truncation active
|
|
40
|
+
* as the last-resort safety net spec.md §9.3 describes; this adapter's own
|
|
41
|
+
* truncation guard (`truncation.ts`) decides the `embedding_truncated`
|
|
42
|
+
* payload flag independently of whether the server actually had to use it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function embed(client: OllamaHttpClient, baseUrl: string, modelName: string, input: string[], signal?: AbortSignal): Promise<OllamaEmbedResponseV1>;
|
|
45
|
+
//# sourceMappingURL=ollama-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama-api.d.ts","sourceRoot":"","sources":["../src/ollama-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC;CACxB;AAED,8FAA8F;AAC9F,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAUrF;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAO/B;AAED;;;;;GAKG;AACH,wBAAsB,KAAK,CACzB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,qBAAqB,CAAC,CAOhC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** `GET /api/tags` — used both as the reachability probe and the model-availability check. */
|
|
2
|
+
export async function listTags(client, baseUrl, signal) {
|
|
3
|
+
return client.requestJson(baseUrl, "api/tags", { method: "GET" }, signal);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* True when `modelTags` (as returned by {@link listTags}) contains
|
|
7
|
+
* `modelName`. Ollama tag names carry an implicit `:latest` suffix when the
|
|
8
|
+
* configured model name has none, so `"nomic-embed-text"` must match a tag
|
|
9
|
+
* named `"nomic-embed-text:latest"`.
|
|
10
|
+
*/
|
|
11
|
+
export function tagsContainModel(modelTags, modelName) {
|
|
12
|
+
const wantsTag = modelName.includes(":");
|
|
13
|
+
return modelTags.some((tag) => {
|
|
14
|
+
const tagName = tag.name;
|
|
15
|
+
if (wantsTag) {
|
|
16
|
+
return tagName === modelName;
|
|
17
|
+
}
|
|
18
|
+
const [tagBase] = tagName.split(":");
|
|
19
|
+
return tagBase === modelName;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `POST /api/pull` with `stream: false` — Ollama blocks until the pull
|
|
24
|
+
* completes (or fails) and returns a single JSON response, which keeps this
|
|
25
|
+
* call symmetric with every other request this client makes (one request,
|
|
26
|
+
* one JSON response, no NDJSON stream parsing).
|
|
27
|
+
*/
|
|
28
|
+
export async function pullModel(client, baseUrl, modelName, signal) {
|
|
29
|
+
return client.requestJson(baseUrl, "api/pull", { method: "POST", body: { model: modelName, stream: false } }, signal);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `POST /api/embed`. `truncate: true` keeps Ollama's own truncation active
|
|
33
|
+
* as the last-resort safety net spec.md §9.3 describes; this adapter's own
|
|
34
|
+
* truncation guard (`truncation.ts`) decides the `embedding_truncated`
|
|
35
|
+
* payload flag independently of whether the server actually had to use it.
|
|
36
|
+
*/
|
|
37
|
+
export async function embed(client, baseUrl, modelName, input, signal) {
|
|
38
|
+
return client.requestJson(baseUrl, "api/embed", { method: "POST", body: { model: modelName, input, truncate: true } }, signal);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=ollama-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama-api.js","sourceRoot":"","sources":["../src/ollama-api.ts"],"names":[],"mappings":"AA0BA,8FAA8F;AAC9F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAwB,EACxB,OAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,CAAuB,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC;AAClG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,SAAwB,EAAE,SAAiB;IAC1E,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;QACzB,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,OAAO,KAAK,SAAS,CAAC;QAC/B,CAAC;QACD,MAAM,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,OAAO,KAAK,SAAS,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,MAAwB,EACxB,OAAe,EACf,SAAiB,EACjB,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,CACvB,OAAO,EACP,UAAU,EACV,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAC7D,MAAM,CACP,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,MAAwB,EACxB,OAAe,EACf,SAAiB,EACjB,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,CACvB,OAAO,EACP,WAAW,EACX,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EACrE,MAAM,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EmbeddingProvider` port implementation for Ollama (domain `ports.ts`,
|
|
3
|
+
* spec.md §9, architecture.md §6.8).
|
|
4
|
+
*/
|
|
5
|
+
import type { Clock, EmbeddingBatchResultV1, EmbeddingProfile, EmbeddingProvider, EmbeddingReadinessV1, PortCallOptions, Random } from "@untangle-code/domain";
|
|
6
|
+
import type { EmbeddingBatchProgressV1, EmbeddingCachePort, OllamaEmbeddingProviderConfig, SleepFn } from "./types.js";
|
|
7
|
+
export interface OllamaEmbeddingProviderDeps {
|
|
8
|
+
clock: Clock;
|
|
9
|
+
random: Random;
|
|
10
|
+
/** Defaults to {@link InMemoryEmbeddingCache} (spec.md §9.4). */
|
|
11
|
+
cache?: EmbeddingCachePort;
|
|
12
|
+
/** Defaults to {@link realSleep}; tests inject a fake that resolves instantly. */
|
|
13
|
+
sleep?: SleepFn;
|
|
14
|
+
/** Defaults to the global `fetch`; tests inject a fake transport. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
export declare class OllamaEmbeddingProvider implements EmbeddingProvider {
|
|
18
|
+
readonly profile: EmbeddingProfile;
|
|
19
|
+
readonly profileId: string;
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly clock;
|
|
22
|
+
private readonly random;
|
|
23
|
+
private readonly cache;
|
|
24
|
+
private readonly sleep;
|
|
25
|
+
private readonly httpClient;
|
|
26
|
+
private readonly backoffConfig;
|
|
27
|
+
private cachedReadiness;
|
|
28
|
+
private resolvedEndpointUrl;
|
|
29
|
+
private readyPromise;
|
|
30
|
+
/**
|
|
31
|
+
* One shared fetch-and-cache operation per still-uncached cache key
|
|
32
|
+
* (spec.md §9.4, WP-78): concurrent `embedDocuments`/`embedQuery` calls
|
|
33
|
+
* that race for the same not-yet-cached text join the SAME `/api/embed`
|
|
34
|
+
* request instead of each starting their own.
|
|
35
|
+
*/
|
|
36
|
+
private readonly inFlightEmbeds;
|
|
37
|
+
constructor(config: OllamaEmbeddingProviderConfig, deps: OllamaEmbeddingProviderDeps);
|
|
38
|
+
ensureReady(options?: PortCallOptions): Promise<EmbeddingReadinessV1>;
|
|
39
|
+
embedDocuments(preparedTexts: string[], options?: PortCallOptions & {
|
|
40
|
+
onBatchProgress?: (progress: EmbeddingBatchProgressV1) => void;
|
|
41
|
+
}): Promise<EmbeddingBatchResultV1>;
|
|
42
|
+
embedQuery(preparedText: string, options?: PortCallOptions): Promise<number[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Runs the readiness sequence exactly once per instance; concurrent
|
|
45
|
+
* callers (including through model-pull, spec.md §9.1 step 4) await the
|
|
46
|
+
* SAME run rather than each starting their own (WP-78 fehlerklasse: the
|
|
47
|
+
* same in-flight-dedup requirement applies here, not just to `/api/embed`).
|
|
48
|
+
*
|
|
49
|
+
* The shared run itself is started with NO caller's `signal` (it is
|
|
50
|
+
* intentionally not tied to whichever caller happened to trigger it) —
|
|
51
|
+
* `raceWithSignal` below still lets THIS caller's own signal reject its
|
|
52
|
+
* own wait promptly, without cancelling the readiness sequence still
|
|
53
|
+
* running for other, still-waiting callers.
|
|
54
|
+
*/
|
|
55
|
+
private ensureReadyOnce;
|
|
56
|
+
private runReadiness;
|
|
57
|
+
private embedTexts;
|
|
58
|
+
private embedSlice;
|
|
59
|
+
/**
|
|
60
|
+
* Fetches every text in `items` in a SINGLE `/api/embed` request and
|
|
61
|
+
* settles each item's `deferred` with its own resulting cache entry, or
|
|
62
|
+
* rejects all of them on failure — which also frees their in-flight slots
|
|
63
|
+
* (`InFlightRegistry.register`'s cleanup), so the next call retries from
|
|
64
|
+
* scratch rather than being stuck on a broken promise (WP-78).
|
|
65
|
+
*
|
|
66
|
+
* Vor dem Absetzen des Requests wird der Cache ein ZWEITES Mal gelesen
|
|
67
|
+
* (WP-117, siehe {@link revalidateBeforeFetch}): die Miss-Entscheidung in
|
|
68
|
+
* `embedSlice` kann veraltet sein, sobald `cache.get()` verzögert auflöst.
|
|
69
|
+
*
|
|
70
|
+
* Intentionally takes no `AbortSignal`: this operation is shared by
|
|
71
|
+
* however many callers joined it, so it is not tied to any single one of
|
|
72
|
+
* their lifecycles (see `embedSlice`/`raceWithSignal`).
|
|
73
|
+
*/
|
|
74
|
+
private fetchAndSettle;
|
|
75
|
+
/**
|
|
76
|
+
* Zweite Cache-Prüfung unmittelbar vor dem Request (WP-117).
|
|
77
|
+
*
|
|
78
|
+
* `embedSlice` trifft die Miss-/Join-Entscheidung EINMAL, aus einem
|
|
79
|
+
* einzigen `Promise.all` über `cache.get()`. Ein Cache, dessen `get()` den
|
|
80
|
+
* Zustand zum Aufrufzeitpunkt liest, aber erst später auflöst — also jeder
|
|
81
|
+
* netzgebundene Cache und damit genau der vorgesehene Erweiterungspunkt
|
|
82
|
+
* für einen persistenten Adapter —, öffnet ein Fenster: Aufrufer A kann
|
|
83
|
+
* zwischen der Anfrage und dem Auflösen von B's `get()` vollständig fertig
|
|
84
|
+
* werden, seinen Eintrag schreiben und seinen In-flight-Platz wieder
|
|
85
|
+
* freigeben. B fände dann weder Cache-Treffer noch In-flight-Eintrag und
|
|
86
|
+
* würde denselben Text ein zweites Mal einbetten.
|
|
87
|
+
*
|
|
88
|
+
* Diese Prüfung schließt das Fenster, weil sie STRIKT NACH der Beanspruchung
|
|
89
|
+
* des In-flight-Platzes liegt: der Platz wird erst freigegeben, nachdem
|
|
90
|
+
* `cache.set()` aufgelöst hat (siehe unten), also sieht ein `get()`, das
|
|
91
|
+
* danach abgesetzt wird, den geschriebenen Eintrag.
|
|
92
|
+
*
|
|
93
|
+
* Kein Deadlock und keine Wiederholungsschleife: genau EIN zusätzliches
|
|
94
|
+
* `cache.get()` je beanspruchtem Schlüssel, kein Warten auf fremde
|
|
95
|
+
* Deferreds (die eigenen Plätze hält dieser Aufruf selbst, ein anderer
|
|
96
|
+
* Aufrufer kann sie also nicht beanspruchen), und jedes Deferred wird auf
|
|
97
|
+
* jedem Pfad festgelegt — aus dem Cache, aus der Antwort oder per
|
|
98
|
+
* `reject` im `catch`.
|
|
99
|
+
*
|
|
100
|
+
* Kein `AbortSignal`: dieser Lauf gehört keinem einzelnen Aufrufer
|
|
101
|
+
* (dieselbe Begründung wie bei {@link fetchAndSettle}).
|
|
102
|
+
*
|
|
103
|
+
* Ein Fehler des Caches wird NICHT verschluckt (E4), sondern nach oben
|
|
104
|
+
* gereicht und weist damit die beteiligten Deferreds ab — dasselbe
|
|
105
|
+
* sichtbare Verhalten wie bei einem Fehlschlag der ERSTEN Cache-Lesung in
|
|
106
|
+
* `embedSlice`. Preis dieser Prüfung: eine zusätzliche Cache-Lesung je
|
|
107
|
+
* echtem Miss (dokumentiert in der README).
|
|
108
|
+
*/
|
|
109
|
+
private revalidateBeforeFetch;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EACV,KAAK,EACL,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,MAAM,EACP,MAAM,uBAAuB,CAAC;AAe/B,OAAO,KAAK,EACV,wBAAwB,EAGxB,kBAAkB,EAClB,6BAA6B,EAC7B,OAAO,EACR,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAoED,qBAAa,uBAAwB,YAAW,iBAAiB;IAC/D,SAAgB,OAAO,EAAE,gBAAgB,CAAC;IAC1C,SAAgB,SAAS,EAAE,MAAM,CAAC;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAE9C,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,YAAY,CAA4B;IAEhD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+C;gBAE3D,MAAM,EAAE,6BAA6B,EAAE,IAAI,EAAE,2BAA2B;IAqB9E,WAAW,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAQrE,cAAc,CACzB,aAAa,EAAE,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,IAAI,CAAA;KAAE,GAC7F,OAAO,CAAC,sBAAsB,CAAC;IAUrB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAQ3F;;;;;;;;;;;OAWG;YACW,eAAe;YAcf,YAAY;YAgBZ,UAAU;YAwCV,UAAU;IA6FxB;;;;;;;;;;;;;;OAcG;YACW,cAAc;IA8C5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;YACW,qBAAqB;CAiBpC"}
|