@tanstack-ai-browser/prompt-api 0.2.0 → 0.2.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -250,7 +250,8 @@ export class PromptApiTextAdapter extends BaseTextAdapter {
250
250
  const completionTokens = Math.max(0, written - markers);
251
251
  return {
252
252
  ...buildBaseUsage({ promptTokens, completionTokens, totalTokens: promptTokens + completionTokens }),
253
- ...(goesOn && { promptTokensDetails: { cachedTokens: held } })
253
+ // A new session read all of it in this step: none was cached.
254
+ promptTokensDetails: { cachedTokens: goesOn ? held : 0 }
254
255
  };
255
256
  }
256
257
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack-ai-browser/prompt-api",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "TanStack AI adapter for the browser's built-in language model (the Prompt API: Gemini Nano in Chrome, Phi-4-mini in Edge)",
5
5
  "license": "MIT",
6
6
  "author": "daveycodez",
@@ -531,7 +531,7 @@ test("a reply reports its tokens: what the session held, and the prompt and the
531
531
  // Less an empty message's count: the turn markers a session counts around any message.
532
532
  const reply = measured("Hello there") - measured("")
533
533
  // A new session read what it holds in this step: nothing cached.
534
- assert.deepEqual(await step(first), { promptTokens: prompt, completionTokens: reply, totalTokens: prompt + reply })
534
+ assert.deepEqual(await step(first), { promptTokens: prompt, completionTokens: reply, totalTokens: prompt + reply, promptTokensDetails: { cachedTokens: 0 } })
535
535
 
536
536
  // The thread's kept session doesn't read what it holds again: only the new message.
537
537
  Object.assign(control.sessions[0], { contextUsage: 300 })
package/src/adapter.ts CHANGED
@@ -348,7 +348,8 @@ export class PromptApiTextAdapter extends BaseTextAdapter<
348
348
  const completionTokens = Math.max(0, written - markers)
349
349
  return {
350
350
  ...buildBaseUsage({ promptTokens, completionTokens, totalTokens: promptTokens + completionTokens }),
351
- ...(goesOn && { promptTokensDetails: { cachedTokens: held } })
351
+ // A new session read all of it in this step: none was cached.
352
+ promptTokensDetails: { cachedTokens: goesOn ? held : 0 }
352
353
  }
353
354
  } catch {
354
355
  return undefined
package/dist/limit.d.ts DELETED
@@ -1,119 +0,0 @@
1
- /**
2
- * Output token limits, emulated. The Prompt API has no output cap (a prompt
3
- * takes only `signal`, `responseConstraint` and
4
- * `omitResponseConstraintInput`), so the adapter holds the reply to a limit
5
- * itself, for `maxCompletionTokens`.
6
- *
7
- * Chrome runs one operation at a time per model, so the model can't count
8
- * text while it writes it. The limit is estimated during the generation
9
- * (from a ratio calibrated on the request before it starts), the generation
10
- * is stopped by an internal abort a little past the limit, and the text is
11
- * measured and trimmed exactly once the model is idle again.
12
- */
13
- import type { LanguageModelSession } from "./types.js";
14
- /** `maxCompletionTokens`, validated like the other adapters validate it. */
15
- export declare function outputTokenLimit(options: {
16
- maxCompletionTokens?: number;
17
- } | undefined): number | undefined;
18
- /** The cap's report in `usage.providerUsageDetails`, when a cap was set. */
19
- export type PromptApiOutputLimitUsage = {
20
- outputTokenLimit: number;
21
- outputTokenMeasurement: "estimate-then-context-usage";
22
- outputTokenLimitEnforcement: "best-effort";
23
- outputTokenLimitReached: boolean;
24
- };
25
- export declare const outputLimitUsage: (limit: number | undefined, reached: boolean) => PromptApiOutputLimitUsage | undefined;
26
- /** A message's text, without its images or audio. */
27
- type TextBearing = {
28
- content: string | ReadonlyArray<{
29
- type: string;
30
- value?: unknown;
31
- }>;
32
- };
33
- /** The text of a prompt input (or of messages), without images or audio. */
34
- export declare function textOfInput(input: string | ReadonlyArray<TextBearing>): string;
35
- /**
36
- * Live emission stops once the estimate passes this share of the limit, or
37
- * once the text passes the limit in UTF-8 bytes. Text sent live can't be
38
- * taken back, so it must stay within the limit whatever the estimate says:
39
- * no token is shorter than a byte, so text of at most `limit` bytes holds at
40
- * most `limit` tokens, however much denser the reply is than the calibration
41
- * text. The share keeps longer limits from streaming up to the last token
42
- * live, where a low estimate would leave nothing to trim.
43
- */
44
- export declare const LIVE_FRACTION = 0.75;
45
- /**
46
- * Generation is stopped once the estimate passes STOP_FACTOR × limit +
47
- * STOP_SLACK tokens: past the limit, so the exact trim afterwards finds the
48
- * limit's worth of text unless the reply holds a fifth fewer tokens per byte
49
- * than the calibration text (the estimate is then high by more than 1.25×).
50
- * The slack covers short limits, where a chunk or two of noise is a large
51
- * share; 8 tokens is about 0.15 s of Gemini Nano's decoding.
52
- */
53
- export declare const STOP_FACTOR = 1.25;
54
- export declare const STOP_SLACK = 8;
55
- /**
56
- * The text a limit's tokens-per-byte ratio is calibrated on: the request's
57
- * own text first, then earlier messages (most recent first) while it's
58
- * under 64 bytes, then a fixed reference sentence if it still is.
59
- */
60
- export declare function calibrationSample(texts: ReadonlyArray<string>): string;
61
- /** What a limiter kept, once the generation has stopped. */
62
- export type LimitResult = {
63
- /** The text within the limit: everything sent live, and `held`. */
64
- text: string;
65
- /** The part of `text` held back during generation, to send now. */
66
- held: string;
67
- /** `text`'s tokens, measured. */
68
- tokens: number;
69
- /**
70
- * The limit ended the text: the exact count reached it, or the limiter
71
- * stopped the generation (even if the estimate ran high and the count
72
- * fell short: the reply was still cut for the limit).
73
- */
74
- reached: boolean;
75
- };
76
- /** Holds streamed text to a token limit without calling the model mid-stream. */
77
- export type TokenLimiter = {
78
- /** All the text received so far. */
79
- readonly text: string;
80
- /** The estimate passed the stop line: stop the generation. */
81
- readonly stopped: boolean;
82
- /**
83
- * Takes the next piece of text and returns what can be sent live now
84
- * (all of it, or nothing once text is held back). Never calls the model.
85
- */
86
- push(delta: string): string;
87
- /**
88
- * Once the generation has stopped (the model is idle, so measuring is
89
- * instant): the exact count, trimmed to the longest prefix within the
90
- * limit, never shorter than what was already sent live.
91
- */
92
- finish(): Promise<LimitResult>;
93
- };
94
- /**
95
- * A limiter for one generation. Chrome runs one operation at a time per
96
- * model: `measureContextUsage` (on any session or clone) and `clone()` wait
97
- * until a generation in progress finishes, so nothing can be measured while
98
- * the model writes. Instead, before generating, `sample` (the request's
99
- * text) is measured to calibrate a bytes-per-token ratio; while generating,
100
- * tokens are estimated from UTF-8 bytes with it; once the generation has
101
- * stopped, `finish()` measures exactly and trims.
102
- *
103
- * Bytes per token, not characters: it varies much less across scripts (a
104
- * CJK character is 3 bytes and about a token; English runs about 4 bytes a
105
- * token), and no token is shorter than a byte, so the ratio's floor of 1 is
106
- * the hard bound tokens ≤ bytes.
107
- */
108
- export declare function tokenLimiter(session: LanguageModelSession, limit: number, sample: string, signal: AbortSignal | undefined): Promise<TokenLimiter>;
109
- /**
110
- * An abort signal for one generation that follows the caller's (with its
111
- * reason) and that `stop()` aborts on its own, never touching the caller's.
112
- */
113
- export declare function internalAbort(signal: AbortSignal | undefined, reason: string): {
114
- signal: AbortSignal;
115
- stop: () => void;
116
- dispose: () => void | undefined;
117
- };
118
- export {};
119
- //# sourceMappingURL=limit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"limit.d.ts","sourceRoot":"","sources":["../src/limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,sBAMrF;AAED,4EAA4E;AAC5E,MAAM,MAAM,yBAAyB,GAAG;IACtC,gBAAgB,EAAE,MAAM,CAAA;IACxB,sBAAsB,EAAE,6BAA6B,CAAA;IACrD,2BAA2B,EAAE,aAAa,CAAA;IAC1C,uBAAuB,EAAE,OAAO,CAAA;CACjC,CAAA;AAED,eAAO,MAAM,gBAAgB,GAC3B,OAAO,MAAM,GAAG,SAAS,EACzB,SAAS,OAAO,KACf,yBAAyB,GAAG,SAQxB,CAAA;AAiBP,qDAAqD;AACrD,KAAK,WAAW,GAAG;IAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAAE,CAAA;AAEzF,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC,WAAW,CAAC,GAAG,MAAM,CAY9E;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,OAAO,CAAA;AACjC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,OAAO,CAAA;AAC/B,eAAO,MAAM,UAAU,IAAI,CAAA;AAY3B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAatE;AAED,4DAA4D;AAC5D,MAAM,MAAM,WAAW,GAAG;IACxB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG;IACzB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,oBAAoB,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,YAAY,CAAC,CAqEvB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM;;;;EAU5E"}
package/dist/limit.js DELETED
@@ -1,203 +0,0 @@
1
- /** `maxCompletionTokens`, validated like the other adapters validate it. */
2
- export function outputTokenLimit(options) {
3
- const limit = options?.maxCompletionTokens;
4
- if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
5
- throw new RangeError("maxCompletionTokens must be a nonnegative safe integer.");
6
- }
7
- return limit;
8
- }
9
- export const outputLimitUsage = (limit, reached) => limit === undefined
10
- ? undefined
11
- : {
12
- outputTokenLimit: limit,
13
- outputTokenMeasurement: "estimate-then-context-usage",
14
- outputTokenLimitEnforcement: "best-effort",
15
- outputTokenLimitReached: reached
16
- };
17
- /** UTF-8 bytes of a string: an upper bound on its tokens. */
18
- function utf8Bytes(text) {
19
- let bytes = 0;
20
- for (let i = 0; i < text.length; i++) {
21
- const code = text.charCodeAt(i);
22
- if (code < 0x80)
23
- bytes += 1;
24
- else if (code < 0x800)
25
- bytes += 2;
26
- else if (code >= 0xd800 && code < 0xdc00 && i + 1 < text.length) {
27
- bytes += 4;
28
- i++;
29
- }
30
- else
31
- bytes += 3;
32
- }
33
- return bytes;
34
- }
35
- /** The text of a prompt input (or of messages), without images or audio. */
36
- export function textOfInput(input) {
37
- if (typeof input === "string")
38
- return input;
39
- return input
40
- .map((message) => typeof message.content === "string"
41
- ? message.content
42
- : message.content
43
- .map((part) => (part.type === "text" && typeof part.value === "string" ? part.value : ""))
44
- .join(""))
45
- .filter(Boolean)
46
- .join("\n");
47
- }
48
- /**
49
- * Live emission stops once the estimate passes this share of the limit, or
50
- * once the text passes the limit in UTF-8 bytes. Text sent live can't be
51
- * taken back, so it must stay within the limit whatever the estimate says:
52
- * no token is shorter than a byte, so text of at most `limit` bytes holds at
53
- * most `limit` tokens, however much denser the reply is than the calibration
54
- * text. The share keeps longer limits from streaming up to the last token
55
- * live, where a low estimate would leave nothing to trim.
56
- */
57
- export const LIVE_FRACTION = 0.75;
58
- /**
59
- * Generation is stopped once the estimate passes STOP_FACTOR × limit +
60
- * STOP_SLACK tokens: past the limit, so the exact trim afterwards finds the
61
- * limit's worth of text unless the reply holds a fifth fewer tokens per byte
62
- * than the calibration text (the estimate is then high by more than 1.25×).
63
- * The slack covers short limits, where a chunk or two of noise is a large
64
- * share; 8 tokens is about 0.15 s of Gemini Nano's decoding.
65
- */
66
- export const STOP_FACTOR = 1.25;
67
- export const STOP_SLACK = 8;
68
- /** Calibration text shorter than this (UTF-8 bytes) is too noisy alone. */
69
- const MIN_SAMPLE_BYTES = 64;
70
- /** Calibration measures at most this many characters of text. */
71
- const MAX_SAMPLE_CHARS = 4000;
72
- /**
73
- * Calibrates when the conversation has almost no text ("hi"): measured with
74
- * the model's own tokenizer like any other text, never a fixed ratio.
75
- */
76
- const REFERENCE_TEXT = "The quick brown fox jumps over the lazy dog, then rests in the warm afternoon sun while the farmer counts his sheep.";
77
- /**
78
- * The text a limit's tokens-per-byte ratio is calibrated on: the request's
79
- * own text first, then earlier messages (most recent first) while it's
80
- * under 64 bytes, then a fixed reference sentence if it still is.
81
- */
82
- export function calibrationSample(texts) {
83
- let sample = "";
84
- for (const text of texts) {
85
- if (!text)
86
- continue;
87
- if (sample && utf8Bytes(sample) >= MIN_SAMPLE_BYTES)
88
- break;
89
- sample = sample ? `${sample}\n${text}` : text;
90
- }
91
- if (utf8Bytes(sample) < MIN_SAMPLE_BYTES) {
92
- sample = sample ? `${sample}\n${REFERENCE_TEXT}` : REFERENCE_TEXT;
93
- }
94
- if (sample.length <= MAX_SAMPLE_CHARS)
95
- return sample;
96
- const end = sample.charCodeAt(MAX_SAMPLE_CHARS - 1);
97
- return sample.slice(0, end >= 0xd800 && end < 0xdc00 ? MAX_SAMPLE_CHARS - 1 : MAX_SAMPLE_CHARS);
98
- }
99
- /**
100
- * A limiter for one generation. Chrome runs one operation at a time per
101
- * model: `measureContextUsage` (on any session or clone) and `clone()` wait
102
- * until a generation in progress finishes, so nothing can be measured while
103
- * the model writes. Instead, before generating, `sample` (the request's
104
- * text) is measured to calibrate a bytes-per-token ratio; while generating,
105
- * tokens are estimated from UTF-8 bytes with it; once the generation has
106
- * stopped, `finish()` measures exactly and trims.
107
- *
108
- * Bytes per token, not characters: it varies much less across scripts (a
109
- * CJK character is 3 bytes and about a token; English runs about 4 bytes a
110
- * token), and no token is shorter than a byte, so the ratio's floor of 1 is
111
- * the hard bound tokens ≤ bytes.
112
- */
113
- export async function tokenLimiter(session, limit, sample, signal) {
114
- const options = signal ? [{ signal }] : [];
115
- let overhead;
116
- const measure = async (text) => {
117
- signal?.throwIfAborted();
118
- if (!text)
119
- return 0;
120
- overhead ??= await session.measureContextUsage("", ...options);
121
- return Math.max(0, (await session.measureContextUsage(text, ...options)) - overhead);
122
- };
123
- const sampleTokens = limit > 0 ? await measure(sample) : 0;
124
- const bytesPerToken = Math.max(1, utf8Bytes(sample) / Math.max(1, sampleTokens));
125
- const liveLine = LIVE_FRACTION * limit;
126
- const stopLine = STOP_FACTOR * limit + STOP_SLACK;
127
- let text = "";
128
- let emitted = "";
129
- let holding = false;
130
- let bytes = 0;
131
- let stopped = limit === 0;
132
- const estimate = () => bytes / bytesPerToken;
133
- const check = () => {
134
- if (estimate() >= stopLine)
135
- stopped = true;
136
- };
137
- return {
138
- get text() { return text; },
139
- get stopped() { return stopped; },
140
- push(delta) {
141
- if (!delta)
142
- return "";
143
- text += delta;
144
- bytes += utf8Bytes(delta);
145
- check();
146
- if (!holding && estimate() <= liveLine && bytes <= limit) {
147
- emitted += delta;
148
- return delta;
149
- }
150
- holding = true;
151
- return "";
152
- },
153
- async finish() {
154
- const tokens = await measure(text);
155
- if (tokens <= limit) {
156
- return { text, held: text.slice(emitted.length), tokens, reached: stopped || tokens >= limit };
157
- }
158
- // Over the limit: keep the longest prefix within it, searching by code
159
- // point from what was sent live. Token counts need not be monotonic in
160
- // the prefix, so the chosen prefix is verified afterwards.
161
- const points = Array.from(text);
162
- const start = Array.from(emitted).length;
163
- let low = start;
164
- let high = points.length - 1;
165
- let accepted = emitted;
166
- while (low <= high) {
167
- const middle = Math.floor((low + high) / 2);
168
- const prefix = points.slice(0, middle).join("");
169
- if ((await measure(prefix)) <= limit) {
170
- accepted = prefix;
171
- low = middle + 1;
172
- }
173
- else
174
- high = middle - 1;
175
- }
176
- let acceptedPoints = Array.from(accepted);
177
- let count = await measure(accepted);
178
- while (count > limit && acceptedPoints.length > start) {
179
- acceptedPoints = acceptedPoints.slice(0, -1);
180
- accepted = acceptedPoints.join("");
181
- count = await measure(accepted);
182
- }
183
- return { text: accepted, held: accepted.slice(emitted.length), tokens: count, reached: true };
184
- }
185
- };
186
- }
187
- /**
188
- * An abort signal for one generation that follows the caller's (with its
189
- * reason) and that `stop()` aborts on its own, never touching the caller's.
190
- */
191
- export function internalAbort(signal, reason) {
192
- const internal = new AbortController();
193
- const cancel = () => internal.abort(signal?.reason);
194
- if (signal?.aborted)
195
- cancel();
196
- else
197
- signal?.addEventListener("abort", cancel, { once: true });
198
- return {
199
- signal: internal.signal,
200
- stop: () => internal.abort(new DOMException(reason, "AbortError")),
201
- dispose: () => signal?.removeEventListener("abort", cancel)
202
- };
203
- }
@@ -1,42 +0,0 @@
1
- import type { ModelMessage, ReasoningRequest } from "@tanstack/ai";
2
- import type { LanguageModelInput, LanguageModelMessage, LanguageModelSession } from "./types.js";
3
- export type PromptApiReasoning = {
4
- levels: "off" | "high";
5
- budget: true;
6
- };
7
- export type PromptApiReasoningUsage = {
8
- reasoningEmulated: true;
9
- reasoningTokenMeasurement: "context-usage";
10
- reasoningBudgetEnforcement: "best-effort";
11
- thinkingTokens: number;
12
- thinkingBudgetTokens?: number;
13
- thinkingBudgetExhausted: boolean;
14
- reasoningMs: number;
15
- };
16
- export type DraftReasoningResult = {
17
- notes: string;
18
- usage: PromptApiReasoningUsage;
19
- capReached: boolean;
20
- };
21
- export declare function promptReasoningOptions(request: ReasoningRequest | undefined): {
22
- enabled: boolean;
23
- budget: number | undefined;
24
- };
25
- /**
26
- * The draft's instruction, quoting the user's latest message: after a tool
27
- * result, the newest user message is this instruction itself, and small
28
- * models took it for the message to reply to.
29
- */
30
- export declare function draftInstruction(messages: ReadonlyArray<ModelMessage>): LanguageModelMessage;
31
- export declare function withWorkingNotes(input: LanguageModelInput, notes: string): LanguageModelInput;
32
- /**
33
- * Two-pass adapter emulation, not hidden native model reasoning or native
34
- * token control. The draft stops at the thinking `budget` and at `cap`, the
35
- * output tokens left for the whole reply (`maxCompletionTokens`, which counts
36
- * the draft too), whichever comes first; `capReached` says the cap stopped it.
37
- * Under a limit, the draft is held to it as `tokenLimiter` holds any reply:
38
- * estimated while it streams, stopped a little past the limit, then measured
39
- * and trimmed exactly (the rest of the notes streams once the draft stops).
40
- */
41
- export declare function draftReasoning(session: LanguageModelSession, input: LanguageModelInput, conversation: ReadonlyArray<ModelMessage>, signal: AbortSignal | undefined, budget?: number, cap?: number): AsyncGenerator<string, DraftReasoningResult>;
42
- //# sourceMappingURL=reasoning.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reasoning.d.ts","sourceRoot":"","sources":["../src/reasoning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAIlE,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEhG,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,IAAI,CAAA;CAAE,CAAA;AACzE,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,yBAAyB,EAAE,eAAe,CAAA;IAC1C,0BAA0B,EAAE,aAAa,CAAA;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,uBAAuB,EAAE,OAAO,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AACD,MAAM,MAAM,oBAAoB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,uBAAuB,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAEzG,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS;;;EAK3E;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAQ5F;AACD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAI7F;AAED;;;;;;;;GAQG;AACH,wBAAuB,cAAc,CAAC,OAAO,EAAE,oBAAoB,EAAE,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAkDvP"}
package/dist/reasoning.js DELETED
@@ -1,113 +0,0 @@
1
- import { calibrationSample, internalAbort, smallest, textOfInput, tokenLimiter } from "./limit.js";
2
- import { contentText } from "./messages.js";
3
- export function promptReasoningOptions(request) {
4
- const budget = request?.budgetTokens;
5
- if (budget !== undefined && (!Number.isSafeInteger(budget) || budget < 0))
6
- throw new RangeError("reasoning.budgetTokens must be a nonnegative safe integer.");
7
- if (request?.level !== undefined && request.level !== "off" && request.level !== "high")
8
- throw new RangeError("Prompt API emulated reasoning supports off or high.");
9
- return { enabled: request?.level === "high", budget };
10
- }
11
- /** The longest quote of the user's message the draft instruction carries. */
12
- const QUOTE_CHARS = 500;
13
- /**
14
- * The draft's instruction, quoting the user's latest message: after a tool
15
- * result, the newest user message is this instruction itself, and small
16
- * models took it for the message to reply to.
17
- */
18
- export function draftInstruction(messages) {
19
- const latest = [...messages].reverse().find((m) => m.role === "user");
20
- const text = latest ? contentText(latest.content).trim().replace(/\s+/g, " ") : "";
21
- const quote = text.length > QUOTE_CHARS ? `${text.slice(0, QUOTE_CHARS - 1)}…` : text;
22
- return {
23
- role: "user",
24
- content: `Now write notes for yourself before replying to the user's latest message${quote ? `, "${quote}"` : ""}: what's needed, what you already have, and what to do next. Don't call tools or write the reply yet.`
25
- };
26
- }
27
- export function withWorkingNotes(input, notes) {
28
- if (!notes)
29
- return input;
30
- const messages = typeof input === "string" ? [{ role: "user", content: input }] : input;
31
- return [...messages, { role: "user", content: `Working notes to verify against the conversation:\n${notes}\nNow produce the final response or required tool call.` }];
32
- }
33
- /**
34
- * Two-pass adapter emulation, not hidden native model reasoning or native
35
- * token control. The draft stops at the thinking `budget` and at `cap`, the
36
- * output tokens left for the whole reply (`maxCompletionTokens`, which counts
37
- * the draft too), whichever comes first; `capReached` says the cap stopped it.
38
- * Under a limit, the draft is held to it as `tokenLimiter` holds any reply:
39
- * estimated while it streams, stopped a little past the limit, then measured
40
- * and trimmed exactly (the rest of the notes streams once the draft stops).
41
- */
42
- export async function* draftReasoning(session, input, conversation, signal, budget, cap) {
43
- signal?.throwIfAborted();
44
- if (budget !== undefined && (!Number.isSafeInteger(budget) || budget < 0))
45
- throw new RangeError("reasoning.budgetTokens must be a nonnegative safe integer.");
46
- const began = performance.now();
47
- const limit = smallest(budget, cap);
48
- const usage = (tokens, exhausted) => ({ reasoningEmulated: true, reasoningTokenMeasurement: "context-usage", reasoningBudgetEnforcement: "best-effort", thinkingTokens: tokens, ...(budget !== undefined && { thinkingBudgetTokens: budget }), thinkingBudgetExhausted: exhausted, reasoningMs: performance.now() - began });
49
- if (limit === 0)
50
- return { notes: "", usage: usage(0, budget === 0), capReached: cap === 0 };
51
- let draft;
52
- let limiter;
53
- const stopper = internalAbort(signal, "Emulated reasoning budget reached");
54
- try {
55
- draft = await session.clone({ signal });
56
- const messages = typeof input === "string" ? [{ role: "user", content: input }] : input;
57
- const prompt = [...messages, draftInstruction(conversation)];
58
- // Calibrated while the model is idle; nothing is measured mid-draft.
59
- if (limit !== undefined)
60
- limiter = await tokenLimiter(session, limit, calibrationSample([textOfInput(prompt)]), signal);
61
- const stream = draft.promptStreaming(prompt, { signal: stopper.signal });
62
- let notes = "";
63
- try {
64
- for await (const piece of stream) {
65
- signal?.throwIfAborted();
66
- const parts = Array.isArray(piece) ? piece : [piece];
67
- const delta = parts.map(part => typeof part === "string" ? part : part.type === "text" ? String(part.value) : "").join("");
68
- if (!delta)
69
- continue;
70
- if (!limiter) {
71
- notes += delta;
72
- yield delta;
73
- continue;
74
- }
75
- const live = limiter.push(delta);
76
- if (limiter.stopped)
77
- stopper.stop();
78
- if (live)
79
- yield live;
80
- if (limiter.stopped)
81
- break;
82
- }
83
- }
84
- catch (error) {
85
- signal?.throwIfAborted();
86
- if (!limiter?.stopped)
87
- throw error;
88
- }
89
- signal?.throwIfAborted();
90
- if (!limiter)
91
- return { notes, usage: usage(await notesTokens(session, notes, signal), false), capReached: false };
92
- // The draft has stopped: the model is idle, so the exact count is instant.
93
- const kept = await limiter.finish();
94
- if (kept.held)
95
- yield kept.held;
96
- return { notes: kept.text, usage: usage(kept.tokens, kept.reached && limit === budget), capReached: kept.reached && limit === cap };
97
- }
98
- finally {
99
- stopper.dispose();
100
- // Stops a draft left unread.
101
- if (!stopper.signal.aborted)
102
- stopper.stop();
103
- draft?.destroy();
104
- }
105
- }
106
- /** Tokens of finished notes, less the empty message's overhead. */
107
- async function notesTokens(session, notes, signal) {
108
- if (!notes)
109
- return 0;
110
- const options = signal ? [{ signal }] : [];
111
- const overhead = await session.measureContextUsage("", ...options);
112
- return Math.max(0, (await session.measureContextUsage(notes, ...options)) - overhead);
113
- }
@@ -1,48 +0,0 @@
1
- import type { PromptMessage } from "./messages.js";
2
- import type { LanguageModelCreateOptions, LanguageModelSession } from "./types.js";
3
- /** A session lent to one model call. */
4
- export type Lease = {
5
- session: LanguageModelSession;
6
- /** Tokens the session had already read and didn't read again. */
7
- cachedTokens: number;
8
- /** Time spent creating the session (reading the conversation), in ms. */
9
- createMs: number;
10
- /** Why the thread's kept session couldn't be used, if it had one. */
11
- miss?: string;
12
- /** Whether this call reused a previously prepared input prefix. */
13
- reused: boolean;
14
- /** Whether the browser dropped old turns to make room since the lease began. */
15
- overflowed: () => boolean;
16
- /** Gives the session back, having read `history` (after the system prompt). */
17
- release: (history: Array<PromptMessage>) => void;
18
- /** Frees this session; for an isolated call, only its working clone is freed. */
19
- discard: () => void;
20
- };
21
- /**
22
- * A session for a conversation that has read `system` and `prior`: the
23
- * thread's own if it has read a start of them (the rest is appended), else
24
- * a new one. `media` caches the images and audio loaded for the call.
25
- */
26
- type SessionOptions = {
27
- threadId: string | undefined;
28
- system: string;
29
- prior: Array<PromptMessage>;
30
- options: LanguageModelCreateOptions;
31
- signal: AbortSignal | undefined;
32
- media?: Map<string, Promise<Blob>>;
33
- /** Generate on a disposable clone of a cached, unmodified input-prefix session. */
34
- isolated?: boolean;
35
- /** Disable managed cross-request reuse and retention for this lease only. */
36
- retain?: boolean;
37
- /** The browser's download of its model, 0 to 1, if a new session starts it. */
38
- onDownloadProgress?: (progress: number) => void;
39
- };
40
- export declare function leaseSession(options: SessionOptions): Promise<Lease>;
41
- /**
42
- * Frees every session kept, as when the page no longer needs the model.
43
- * Sessions a call holds, or is still creating, finish that call and are
44
- * freed when it ends instead of going back in the cache.
45
- */
46
- export declare function destroyPromptApiSessions(): void;
47
- export {};
48
- //# sourceMappingURL=sessions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,KAAK,EACV,0BAA0B,EAC1B,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAwBnB,wCAAwC;AACxC,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE,oBAAoB,CAAA;IAC7B,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAA;IACpB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mEAAmE;IACnE,MAAM,EAAE,OAAO,CAAA;IACf,gFAAgF;IAChF,UAAU,EAAE,MAAM,OAAO,CAAA;IACzB,+EAA+E;IAC/E,OAAO,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IAChD,iFAAiF;IACjF,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED;;;;GAIG;AACH,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAA;IAC3B,OAAO,EAAE,0BAA0B,CAAA;IACnC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAA;IAC/B,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAClC,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAChD,CAAA;AA6BD,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,CA8D1E;AA4KD;;;;GAIG;AACH,wBAAgB,wBAAwB,SAQvC"}
package/dist/sessions.js DELETED
@@ -1,275 +0,0 @@
1
- /**
2
- * Sessions by conversation. A Prompt API session holds the conversation
3
- * it has read (the model's cache), so each thread keeps its own: when the
4
- * next request extends what the session has read (a follow-up, or the step
5
- * after a tool call), only the new messages are read. Anything else (an
6
- * edited or regenerated message, another system prompt) starts a new
7
- * session from the whole conversation.
8
- */
9
- import { createLanguageModel } from "./model.js";
10
- import { keyOf, toLanguageModelMessages } from "./messages.js";
11
- /** Sessions kept at once, most recently used first; older ones are freed. */
12
- const MAX_SESSIONS = 4;
13
- const entries = new Map();
14
- const preparing = new Map();
15
- /** Counts unloads: a session created across one isn't cached. */
16
- let generation = 0;
17
- const startsWith = (list, start) => start.length <= list.length && start.every((key, i) => list[i] === key);
18
- /** Serialize base preparation only; independent clones generate concurrently. */
19
- async function lockPreparation(key, signal) {
20
- const previous = preparing.get(key) ?? Promise.resolve();
21
- let unlock = () => undefined;
22
- const done = new Promise(resolve => { unlock = resolve; });
23
- const queued = previous.then(() => done);
24
- preparing.set(key, queued);
25
- void queued.then(() => { if (preparing.get(key) === queued)
26
- preparing.delete(key); });
27
- try {
28
- await new Promise((resolve, reject) => {
29
- const cancel = () => reject(signal?.reason);
30
- signal?.addEventListener("abort", cancel, { once: true });
31
- void previous.then(() => {
32
- signal?.removeEventListener("abort", cancel);
33
- if (signal?.aborted)
34
- reject(signal.reason);
35
- else
36
- resolve();
37
- });
38
- });
39
- return unlock;
40
- }
41
- catch (error) {
42
- // An aborted waiter releases its place without letting later callers pass
43
- // the earlier preparation that is still in flight.
44
- unlock();
45
- throw error;
46
- }
47
- }
48
- export async function leaseSession(options) {
49
- options.signal?.throwIfAborted();
50
- const { threadId, isolated, system, prior } = options;
51
- const setup = JSON.stringify({ system, options: options.options });
52
- // Separate namespaces prevent structured finalization from replacing a chat/tool
53
- // session. Stateless calls can share an identical base without inventing a thread.
54
- const cacheKey = options.retain === false ? undefined : isolated
55
- ? JSON.stringify(["structured", threadId ?? null, threadId ? null : setup])
56
- : threadId ? JSON.stringify(["chat", threadId]) : undefined;
57
- const unlock = isolated && cacheKey !== undefined ? await lockPreparation(cacheKey, options.signal) : undefined;
58
- try {
59
- let recreated = false;
60
- for (;;) {
61
- const base = await leasePooledSession({ ...options, cacheKey });
62
- // A non-retained session is already isolated: generate directly on it and
63
- // destroy it when the call ends, without preparing an extra clone.
64
- if (!isolated || options.retain === false)
65
- return base;
66
- const started = performance.now();
67
- let copy;
68
- try {
69
- copy = await base.session.clone({ signal: options.signal });
70
- options.signal?.throwIfAborted();
71
- let overflowed = false;
72
- const onOverflow = () => { overflowed = true; };
73
- copy.addEventListener("contextoverflow", onOverflow);
74
- const baseOverflowed = base.overflowed();
75
- // Only preparation touches the base. Release it before generation so concurrent
76
- // callers can clone it too; generated JSON and draft notes never enter it.
77
- base.release(prior);
78
- const session = copy;
79
- let destroyed = false;
80
- const destroy = () => {
81
- if (destroyed)
82
- return;
83
- destroyed = true;
84
- session.removeEventListener("contextoverflow", onOverflow);
85
- session.destroy();
86
- };
87
- return {
88
- ...base,
89
- session,
90
- createMs: base.createMs + performance.now() - started,
91
- overflowed: () => baseOverflowed || overflowed,
92
- release: destroy,
93
- discard: destroy
94
- };
95
- }
96
- catch (error) {
97
- copy?.destroy();
98
- // Aborting a clone must not destroy its healthy cached parent.
99
- if (options.signal?.aborted)
100
- base.release(prior);
101
- else
102
- base.discard();
103
- // A stale browser session can fail at clone() before any generation starts.
104
- // Recreate that prefix once, just as the streaming path does.
105
- if (!options.signal?.aborted && base.reused && !recreated && error instanceof DOMException && error.name === "InvalidStateError") {
106
- recreated = true;
107
- continue;
108
- }
109
- throw error;
110
- }
111
- }
112
- }
113
- finally {
114
- unlock?.();
115
- }
116
- }
117
- async function leasePooledSession({ cacheKey, system, prior, options, signal, media, onDownloadProgress }) {
118
- const setup = JSON.stringify({ system, options });
119
- const keys = prior.map(keyOf);
120
- const entry = cacheKey ? entries.get(cacheKey) : undefined;
121
- let miss;
122
- if (entry) {
123
- miss = entry.busy
124
- ? "another call of this thread holds its session"
125
- : entry.setup !== setup
126
- ? "the system prompt, tools or options changed"
127
- : startsWith(keys, entry.history)
128
- ? undefined
129
- : "the conversation changed (an edited, regenerated or rewritten message)";
130
- }
131
- if (entry && !miss) {
132
- entry.busy = true;
133
- // Most recently used last.
134
- entries.delete(cacheKey);
135
- entries.set(cacheKey, entry);
136
- const cachedTokens = entry.session.contextUsage;
137
- const started = performance.now();
138
- const gap = prior.slice(entry.history.length);
139
- entry.overflowed = false;
140
- try {
141
- if (gap.length > 0) {
142
- await entry.session.append(await toLanguageModelMessages(gap, media), {
143
- signal
144
- });
145
- }
146
- entry.history = keys;
147
- return {
148
- ...lease(cacheKey, entry, cachedTokens, performance.now() - started),
149
- reused: true
150
- };
151
- }
152
- catch (error) {
153
- drop(cacheKey, entry);
154
- if (signal?.aborted)
155
- throw error;
156
- // The session couldn't take them (it was destroyed, say): start over.
157
- miss = `the kept session failed (${error instanceof Error ? error.message : String(error)})`;
158
- }
159
- }
160
- // Another run of this thread holds its session (it shouldn't happen):
161
- // this one gets a session of its own, freed when it ends.
162
- const shared = !entry?.busy;
163
- if (entry && shared && cacheKey)
164
- drop(cacheKey, entry);
165
- const startedIn = generation;
166
- const started = performance.now();
167
- const created = {
168
- session: await createSession(system, prior, options, signal, media, onDownloadProgress),
169
- setup,
170
- history: keys,
171
- busy: true,
172
- overflowed: false
173
- };
174
- created.session.addEventListener("contextoverflow", () => {
175
- created.overflowed = true;
176
- });
177
- // Unloaded while it was created: the call uses it, then it's freed.
178
- const kept = cacheKey !== undefined && shared && startedIn === generation;
179
- if (kept) {
180
- entries.set(cacheKey, created);
181
- trimIdleSessions();
182
- }
183
- return {
184
- ...lease(kept ? cacheKey : undefined, created, 0, performance.now() - started),
185
- reused: false,
186
- ...(miss && { miss })
187
- };
188
- }
189
- function lease(threadId, entry, cachedTokens, createMs) {
190
- return {
191
- session: entry.session,
192
- cachedTokens,
193
- createMs,
194
- overflowed: () => entry.overflowed,
195
- release: (history) => {
196
- if (!threadId || entries.get(threadId) !== entry)
197
- return entry.session.destroy();
198
- entry.history = history.map(keyOf);
199
- entry.busy = false;
200
- trimIdleSessions();
201
- },
202
- discard: () => {
203
- if (threadId)
204
- drop(threadId, entry);
205
- else
206
- entry.session.destroy();
207
- }
208
- };
209
- }
210
- /** Busy entries can exceed the bound temporarily; evict oldest idle entries on return. */
211
- function trimIdleSessions() {
212
- for (const [id, entry] of entries) {
213
- if (entries.size <= MAX_SESSIONS)
214
- break;
215
- if (!entry.busy)
216
- drop(id, entry);
217
- }
218
- }
219
- function drop(threadId, entry) {
220
- if (entries.get(threadId) === entry)
221
- entries.delete(threadId);
222
- entry.session.destroy();
223
- }
224
- /** The conversation without its oldest turn: up to the next user message. */
225
- function withoutOldestTurn(messages) {
226
- const next = messages.findIndex((m, i) => i > 0 && m.role === "user");
227
- return next > 0 ? messages.slice(next) : [];
228
- }
229
- /**
230
- * A new session that has read the system prompt and the conversation. Only
231
- * the system prompt is an initial prompt, which the browser never drops;
232
- * the conversation is appended, so when it later outgrows the context the
233
- * browser drops its oldest turns. If it doesn't fit now, its oldest turns
234
- * are left out (measured first, not tried) until it does.
235
- */
236
- async function createSession(system, prior, options, signal, media, onDownloadProgress) {
237
- const session = await createLanguageModel({
238
- ...options,
239
- ...(system && { initialPrompts: [{ role: "system", content: system }] })
240
- }, signal, onDownloadProgress);
241
- try {
242
- let rest = prior;
243
- while (rest.length > 0) {
244
- const input = await toLanguageModelMessages(rest, media);
245
- const room = session.contextWindow - session.contextUsage;
246
- const needed = await session.measureContextUsage(input, { signal });
247
- if (needed <= room) {
248
- await session.append(input, { signal });
249
- break;
250
- }
251
- rest = withoutOldestTurn(rest);
252
- }
253
- return session;
254
- }
255
- catch (error) {
256
- session.destroy();
257
- throw error;
258
- }
259
- }
260
- /**
261
- * Frees every session kept, as when the page no longer needs the model.
262
- * Sessions a call holds, or is still creating, finish that call and are
263
- * freed when it ends instead of going back in the cache.
264
- */
265
- export function destroyPromptApiSessions() {
266
- generation++;
267
- for (const [id, entry] of [...entries]) {
268
- // A busy entry is only detached: its lease sees it's no longer the
269
- // thread's and destroys the session on release.
270
- if (entry.busy)
271
- entries.delete(id);
272
- else
273
- drop(id, entry);
274
- }
275
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * The callers waiting on one shared model load: their progress callbacks
3
- * and how many still wait. A caller whose signal aborts stops waiting at
4
- * once and hears nothing more; when the last caller still waiting aborts,
5
- * the load is cancelled. (Kept in step with the same file in the other
6
- * runtime packages.)
7
- */
8
- /** A download's progress, from 0 to 1. */
9
- type DownloadProgressCallback = (progress: number) => void;
10
- /** What an aborted `create()` rejects with. */
11
- export declare function abortError(): DOMException;
12
- /** Calls a progress callback; one that throws doesn't stop the load. */
13
- export declare function callProgress(listener: DownloadProgressCallback, progress: number): void;
14
- export type WaitOptions = {
15
- onDownloadProgress?: DownloadProgressCallback;
16
- signal?: AbortSignal;
17
- /**
18
- * Whether this caller aborting last cancels the load (true by default).
19
- * A chat's own wait passes false: it stops waiting, and the download goes
20
- * on for the next chat.
21
- */
22
- cancels?: boolean;
23
- /** What this caller rejects with when it aborts: an AbortError by default. */
24
- reason?: (signal: AbortSignal) => unknown;
25
- };
26
- export declare class LoadWaiters {
27
- private readonly cancel;
28
- /** Each callback, with how many waiting callers use it: called once per report. */
29
- private readonly callbacks;
30
- private waiting;
31
- private settled;
32
- /** `cancel` stops the load: called when the last caller still waiting aborts. */
33
- constructor(cancel: () => void);
34
- /** Reports progress to the callers still waiting. */
35
- report(progress: number): void;
36
- /** The load settled (ready, failed or replaced): nothing more to report or cancel. */
37
- settle(): void;
38
- /**
39
- * `load`'s result for one more caller. Aborting `signal` rejects this
40
- * caller at once and stops its reports; if it was the last caller waiting
41
- * (and `cancels`), the load is cancelled. The promise is fine to leave
42
- * unawaited. Check the signal before starting the load: an aborted one
43
- * rejects here without waiting.
44
- */
45
- wait<T>(load: Promise<T>, options?: WaitOptions): Promise<T>;
46
- }
47
- export {};
48
- //# sourceMappingURL=shared-load.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared-load.d.ts","sourceRoot":"","sources":["../src/shared-load.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0CAA0C;AAC1C,KAAK,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;AAE1D,+CAA+C;AAC/C,wBAAgB,UAAU,IAAI,YAAY,CAEzC;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,wBAAwB,EAAE,QAAQ,EAAE,MAAM,QAMhF;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kBAAkB,CAAC,EAAE,wBAAwB,CAAA;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAA;CAC1C,CAAA;AAED,qBAAa,WAAW;IAOV,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8C;IACxE,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,OAAO,CAAQ;IAEvB,iFAAiF;gBACpD,MAAM,EAAE,MAAM,IAAI;IAE/C,qDAAqD;IACrD,MAAM,CAAC,QAAQ,EAAE,MAAM;IAIvB,sFAAsF;IACtF,MAAM;IAMN;;;;;;OAMG;IACH,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;CA8CjE"}
@@ -1,103 +0,0 @@
1
- /**
2
- * The callers waiting on one shared model load: their progress callbacks
3
- * and how many still wait. A caller whose signal aborts stops waiting at
4
- * once and hears nothing more; when the last caller still waiting aborts,
5
- * the load is cancelled. (Kept in step with the same file in the other
6
- * runtime packages.)
7
- */
8
- /** What an aborted `create()` rejects with. */
9
- export function abortError() {
10
- return new DOMException("The model load was aborted.", "AbortError");
11
- }
12
- /** Calls a progress callback; one that throws doesn't stop the load. */
13
- export function callProgress(listener, progress) {
14
- try {
15
- listener(progress);
16
- }
17
- catch (error) {
18
- console.error(error);
19
- }
20
- }
21
- export class LoadWaiters {
22
- cancel;
23
- /** Each callback, with how many waiting callers use it: called once per report. */
24
- callbacks = new Map();
25
- waiting = 0;
26
- settled = false;
27
- /** `cancel` stops the load: called when the last caller still waiting aborts. */
28
- constructor(cancel) {
29
- this.cancel = cancel;
30
- }
31
- /** Reports progress to the callers still waiting. */
32
- report(progress) {
33
- for (const listener of [...this.callbacks.keys()])
34
- callProgress(listener, progress);
35
- }
36
- /** The load settled (ready, failed or replaced): nothing more to report or cancel. */
37
- settle() {
38
- this.settled = true;
39
- this.callbacks.clear();
40
- this.waiting = 0;
41
- }
42
- /**
43
- * `load`'s result for one more caller. Aborting `signal` rejects this
44
- * caller at once and stops its reports; if it was the last caller waiting
45
- * (and `cancels`), the load is cancelled. The promise is fine to leave
46
- * unawaited. Check the signal before starting the load: an aborted one
47
- * rejects here without waiting.
48
- */
49
- wait(load, options = {}) {
50
- const { onDownloadProgress, signal, cancels = true, reason = abortError } = options;
51
- if (signal?.aborted)
52
- return quiet(Promise.reject(reason(signal)));
53
- if (this.settled)
54
- return quiet(load);
55
- this.waiting++;
56
- if (onDownloadProgress) {
57
- this.callbacks.set(onDownloadProgress, (this.callbacks.get(onDownloadProgress) ?? 0) + 1);
58
- }
59
- let left = false;
60
- const leave = () => {
61
- if (left)
62
- return;
63
- left = true;
64
- if (this.settled)
65
- return;
66
- this.waiting--;
67
- if (!onDownloadProgress)
68
- return;
69
- const count = (this.callbacks.get(onDownloadProgress) ?? 0) - 1;
70
- if (count > 0)
71
- this.callbacks.set(onDownloadProgress, count);
72
- else
73
- this.callbacks.delete(onDownloadProgress);
74
- };
75
- if (!signal) {
76
- load.then(leave, leave);
77
- return quiet(load);
78
- }
79
- return quiet(new Promise((resolve, reject) => {
80
- const onAbort = () => {
81
- leave();
82
- reject(reason(signal));
83
- if (cancels && !this.settled && this.waiting === 0)
84
- this.cancel();
85
- };
86
- signal.addEventListener("abort", onAbort, { once: true });
87
- load.then((value) => {
88
- signal.removeEventListener("abort", onAbort);
89
- leave();
90
- resolve(value);
91
- }, (error) => {
92
- signal.removeEventListener("abort", onAbort);
93
- leave();
94
- reject(error);
95
- });
96
- }));
97
- }
98
- }
99
- /** `promise`, with a rejection that's fine to leave unhandled. */
100
- function quiet(promise) {
101
- promise.catch(() => undefined);
102
- return promise;
103
- }
package/dist/store.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /** A value that can be read, set and watched. Framework-free. */
2
- export type Store<T> = {
3
- get: () => T;
4
- set: (value: T) => void;
5
- subscribe: (listener: () => void) => () => void;
6
- };
7
- export declare function createStore<T>(initial: T): Store<T>;
8
- //# sourceMappingURL=store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI;IACrB,GAAG,EAAE,MAAM,CAAC,CAAA;IACZ,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;IACvB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAgBnD"}
package/dist/store.js DELETED
@@ -1,17 +0,0 @@
1
- export function createStore(initial) {
2
- let value = initial;
3
- const listeners = new Set();
4
- return {
5
- get: () => value,
6
- set: (next) => {
7
- value = next;
8
- listeners.forEach((listener) => listener());
9
- },
10
- subscribe: (listener) => {
11
- listeners.add(listener);
12
- return () => {
13
- listeners.delete(listener);
14
- };
15
- }
16
- };
17
- }