@animalabs/membrane 0.5.79 → 0.5.81
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/dist/cache-keepalive.d.ts +115 -0
- package/dist/cache-keepalive.d.ts.map +1 -0
- package/dist/cache-keepalive.js +0 -0
- package/dist/cache-keepalive.js.map +1 -0
- package/dist/cache-keepalive.test.d.ts +2 -0
- package/dist/cache-keepalive.test.d.ts.map +1 -0
- package/dist/cache-keepalive.test.js +206 -0
- package/dist/cache-keepalive.test.js.map +1 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/process.d.ts +3 -1
- package/dist/context/process.d.ts.map +1 -1
- package/dist/context/process.js +230 -49
- package/dist/context/process.js.map +1 -1
- package/dist/context/types.d.ts +30 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +16 -0
- package/dist/context/types.js.map +1 -1
- package/dist/floating-cache-marker.test.d.ts +2 -0
- package/dist/floating-cache-marker.test.d.ts.map +1 -0
- package/dist/floating-cache-marker.test.js +263 -0
- package/dist/floating-cache-marker.test.js.map +1 -0
- package/dist/formatters/anthropic-xml.d.ts +2 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +57 -19
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
- package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
- package/dist/formatters/normalize-tool-pairs.js +45 -0
- package/dist/formatters/normalize-tool-pairs.js.map +1 -1
- package/dist/formatters/types.d.ts +29 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +142 -7
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +881 -270
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +163 -26
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +24 -1
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +104 -82
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +2 -0
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +7 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +99 -48
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +5 -0
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +107 -44
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +16 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +38 -10
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +2 -0
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +8 -1
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -44
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +9 -0
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +118 -62
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/utils.d.ts +74 -2
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +184 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/registry/default-pricing.d.ts +22 -0
- package/dist/registry/default-pricing.d.ts.map +1 -1
- package/dist/registry/default-pricing.js +162 -28
- package/dist/registry/default-pricing.js.map +1 -1
- package/dist/types/config.d.ts +5 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/content.d.ts +13 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +26 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +50 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/request.d.ts +13 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/response.d.ts +85 -4
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/response.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -1
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/cache-marker-budget.d.ts +82 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +189 -0
- package/dist/utils/cache-marker-budget.js.map +1 -0
- package/dist/utils/cost.d.ts +22 -0
- package/dist/utils/cost.d.ts.map +1 -1
- package/dist/utils/cost.js +55 -0
- package/dist/utils/cost.js.map +1 -1
- package/dist/utils/thinking-carriers.d.ts +80 -0
- package/dist/utils/thinking-carriers.d.ts.map +1 -0
- package/dist/utils/thinking-carriers.js +115 -0
- package/dist/utils/thinking-carriers.js.map +1 -0
- package/dist/utils/tool-parser.d.ts +24 -0
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +367 -161
- package/dist/utils/tool-parser.js.map +1 -1
- package/dist/utils/usage.d.ts +71 -0
- package/dist/utils/usage.d.ts.map +1 -0
- package/dist/utils/usage.js +161 -0
- package/dist/utils/usage.js.map +1 -0
- package/dist/yielding-stream.d.ts +8 -0
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +49 -3
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +3 -2
- package/src/cache-keepalive.test.ts +244 -0
- package/src/cache-keepalive.ts +385 -0
- package/src/context/index.ts +1 -0
- package/src/context/process.ts +305 -59
- package/src/context/types.ts +38 -0
- package/src/floating-cache-marker.test.ts +285 -0
- package/src/formatters/anthropic-xml.ts +62 -20
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +32 -0
- package/src/index.ts +13 -0
- package/src/membrane.ts +1031 -279
- package/src/providers/anthropic.ts +205 -20
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-compatible.ts +102 -45
- package/src/providers/openai-completions.ts +112 -46
- package/src/providers/openai-responses-api.ts +45 -15
- package/src/providers/openai-responses.ts +8 -1
- package/src/providers/openai.ts +95 -42
- package/src/providers/openrouter.ts +125 -63
- package/src/providers/utils.ts +227 -6
- package/src/registry/default-pricing.ts +165 -28
- package/src/types/config.ts +6 -0
- package/src/types/content.ts +14 -0
- package/src/types/errors.ts +28 -0
- package/src/types/index.ts +6 -0
- package/src/types/provider.ts +55 -0
- package/src/types/request.ts +14 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +208 -0
- package/src/utils/cost.ts +64 -0
- package/src/utils/thinking-carriers.ts +125 -0
- package/src/utils/tool-parser.ts +473 -174
- package/src/utils/usage.ts +184 -0
- package/src/yielding-stream.ts +54 -3
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ModelPricing, ProviderResponse, UsageCacheConvention } from '../types/provider.js';
|
|
2
|
+
import type { CostBreakdown, DetailedUsage, TurnRoundUsage } from '../types/response.js';
|
|
3
|
+
import { addCostBreakdowns, calculateCost, warnUnpricedModel } from './cost.js';
|
|
4
|
+
|
|
5
|
+
/** Adapters whose convention is undeclared warn once each, not once per call. */
|
|
6
|
+
const warnedUndeclaredAdapters = new Set<string>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Restate a provider's usage in membrane's single convention: `inputTokens`,
|
|
10
|
+
* `cacheReadTokens` and `cacheCreationTokens` DISJOINT, each priced at its own
|
|
11
|
+
* rate. Adapters reporting `cache-inclusive` counts have the cached span
|
|
12
|
+
* subtracted out of `inputTokens`; the total prompt size stays recoverable as
|
|
13
|
+
* `inputTokens + cacheReadTokens`.
|
|
14
|
+
*
|
|
15
|
+
* Without this, one field name carried two incomparable meanings — the cache
|
|
16
|
+
* hit ratio was unbounded on cache-excluded adapters and cost double-charged
|
|
17
|
+
* the cached span on cache-inclusive ones.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeUsageToCacheExcluded(
|
|
20
|
+
usage: ProviderResponse['usage'],
|
|
21
|
+
adapterName: string,
|
|
22
|
+
adapterConvention: UsageCacheConvention | undefined,
|
|
23
|
+
): ProviderResponse['usage'] {
|
|
24
|
+
// An adapter that declares nothing is in exactly the state `unknown` names,
|
|
25
|
+
// which is why the interface field is optional and lands here.
|
|
26
|
+
const convention = usage.cacheConvention ?? adapterConvention ?? 'unknown';
|
|
27
|
+
const cacheReadTokens = usage.cacheReadTokens ?? 0;
|
|
28
|
+
|
|
29
|
+
if (convention === 'unknown' && cacheReadTokens > 0 && !warnedUndeclaredAdapters.has(adapterName)) {
|
|
30
|
+
warnedUndeclaredAdapters.add(adapterName);
|
|
31
|
+
console.warn(
|
|
32
|
+
`[membrane:usage] adapter "${adapterName}" reported ${cacheReadTokens} cache-read tokens but`
|
|
33
|
+
+ ' declares usageCacheConvention "unknown", so membrane cannot tell whether inputTokens'
|
|
34
|
+
+ ` (${usage.inputTokens}) already includes them. Counts are passed through unchanged;`
|
|
35
|
+
+ ' cost and cache hit ratio may be off by the cached span until the adapter declares'
|
|
36
|
+
+ ' "cache-excluded" or "cache-inclusive".'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (convention !== 'cache-inclusive' || cacheReadTokens === 0) return usage;
|
|
41
|
+
|
|
42
|
+
return { ...usage, inputTokens: Math.max(0, usage.inputTokens - cacheReadTokens) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Share of the prompt that was served from cache. Both terms are in the
|
|
47
|
+
* cache-excluded convention, so this is a true ratio in [0, 1] regardless of
|
|
48
|
+
* which provider produced the counts.
|
|
49
|
+
*/
|
|
50
|
+
export function calculateCacheHitRatio(
|
|
51
|
+
usage: { inputTokens: number; cacheReadTokens?: number },
|
|
52
|
+
): number {
|
|
53
|
+
const cacheReadTokens = usage.cacheReadTokens ?? 0;
|
|
54
|
+
const promptTokens = (usage.inputTokens ?? 0) + cacheReadTokens;
|
|
55
|
+
if (promptTokens === 0) return 0;
|
|
56
|
+
return cacheReadTokens / promptTokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Test seam: the once-per-adapter warn latch is process-wide otherwise. */
|
|
60
|
+
export function resetUndeclaredConventionWarnings(): void {
|
|
61
|
+
warnedUndeclaredAdapters.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Unconvertible provider item types warn once each, not once per response. */
|
|
65
|
+
const warnedUnconvertibleProviderItems = new Set<string>();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A provider content item membrane cannot normalize is preserved verbatim on a
|
|
69
|
+
* zero-width carrier rather than dropped, but the caller should know its
|
|
70
|
+
* content is invisible to normalized consumers. Once per type, not per item.
|
|
71
|
+
*/
|
|
72
|
+
export function warnUnconvertibleProviderItem(itemType: string): void {
|
|
73
|
+
if (warnedUnconvertibleProviderItems.has(itemType)) return;
|
|
74
|
+
warnedUnconvertibleProviderItems.add(itemType);
|
|
75
|
+
console.warn(
|
|
76
|
+
`[membrane:content] no normalized ContentBlock for provider item type "${itemType}"`
|
|
77
|
+
+ ' — preserving it verbatim as a rawItem carrier so it can be replayed, but its'
|
|
78
|
+
+ ' content is not visible to normalized consumers.'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test seam: the once-per-type warn latch is process-wide otherwise. */
|
|
83
|
+
export function resetUnconvertibleProviderItemWarnings(): void {
|
|
84
|
+
warnedUnconvertibleProviderItems.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Accumulates one turn's usage ACROSS ROUNDS, pricing each round under the
|
|
89
|
+
* model that actually served it.
|
|
90
|
+
*
|
|
91
|
+
* A routed turn is not billed at one rate: OpenRouter re-picks a provider per
|
|
92
|
+
* call and an alias can resolve to a new snapshot mid-turn. Every streaming
|
|
93
|
+
* loop used to re-resolve pricing when the served model changed and then
|
|
94
|
+
* recompute the WHOLE accumulated usage at that latest rate, retroactively
|
|
95
|
+
* re-billing every earlier round — two 1M-token rounds at 1,000/M then
|
|
96
|
+
* 10,000/M reported 20,000 against a real 11,000.
|
|
97
|
+
*
|
|
98
|
+
* The four tool loops (callback/yielding × XML/native) each carried their own
|
|
99
|
+
* copy of the accumulate-and-price block; this is the single one they share.
|
|
100
|
+
*/
|
|
101
|
+
export class TurnUsageAccumulator {
|
|
102
|
+
private readonly rounds: TurnRoundUsage[] = [];
|
|
103
|
+
private readonly tokens: DetailedUsage = { inputTokens: 0, outputTokens: 0 };
|
|
104
|
+
private summedCost: CostBreakdown | undefined;
|
|
105
|
+
/** False once any round could not be priced, or two rounds disagreed on currency. */
|
|
106
|
+
private costCoversEveryRound = true;
|
|
107
|
+
private lastServed: string | undefined;
|
|
108
|
+
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly requestedModel: string,
|
|
111
|
+
private readonly resolveRoundPricing: (servedModel?: string) => ModelPricing | undefined,
|
|
112
|
+
) {}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fold one provider round in, priced at its own served model, and return the
|
|
116
|
+
* turn total as it now stands (a fresh snapshot — callers hand this to
|
|
117
|
+
* `onUsage` and to stream events, and must not see it mutate underneath).
|
|
118
|
+
*/
|
|
119
|
+
addRound(servedModel: string | undefined, usage: ProviderResponse['usage']): DetailedUsage {
|
|
120
|
+
if (servedModel) this.lastServed = servedModel;
|
|
121
|
+
|
|
122
|
+
this.tokens.inputTokens += usage.inputTokens;
|
|
123
|
+
this.tokens.outputTokens += usage.outputTokens;
|
|
124
|
+
if (usage.cacheCreationTokens) {
|
|
125
|
+
this.tokens.cacheCreationTokens = (this.tokens.cacheCreationTokens ?? 0) + usage.cacheCreationTokens;
|
|
126
|
+
}
|
|
127
|
+
if (usage.cacheReadTokens) {
|
|
128
|
+
this.tokens.cacheReadTokens = (this.tokens.cacheReadTokens ?? 0) + usage.cacheReadTokens;
|
|
129
|
+
}
|
|
130
|
+
// Already inside outputTokens (billed at the output rate) — summed for
|
|
131
|
+
// attribution, never added to the output total.
|
|
132
|
+
if (usage.thinkingTokens) {
|
|
133
|
+
this.tokens.thinkingTokens = (this.tokens.thinkingTokens ?? 0) + usage.thinkingTokens;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const roundModel = servedModel || this.requestedModel;
|
|
137
|
+
const roundPricing = this.resolveRoundPricing(servedModel);
|
|
138
|
+
const roundCost = roundPricing ? calculateCost(usage, roundPricing) : undefined;
|
|
139
|
+
if (!roundPricing) warnUnpricedModel(roundModel);
|
|
140
|
+
|
|
141
|
+
this.rounds.push({
|
|
142
|
+
model: roundModel,
|
|
143
|
+
usage: {
|
|
144
|
+
inputTokens: usage.inputTokens,
|
|
145
|
+
outputTokens: usage.outputTokens,
|
|
146
|
+
...(usage.cacheCreationTokens != null ? { cacheCreationTokens: usage.cacheCreationTokens } : {}),
|
|
147
|
+
...(usage.cacheReadTokens != null ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
148
|
+
...(usage.thinkingTokens != null ? { thinkingTokens: usage.thinkingTokens } : {}),
|
|
149
|
+
...(roundCost ? { estimatedCost: roundCost } : {}),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// A partial sum presented as the turn total is a false number, and an
|
|
154
|
+
// absent cost already means "membrane does not know". So one unpriced (or
|
|
155
|
+
// unsummable) round drops the total rather than under-reporting it; the
|
|
156
|
+
// rounds that WERE priced stay readable in the roster.
|
|
157
|
+
if (!roundCost) {
|
|
158
|
+
this.costCoversEveryRound = false;
|
|
159
|
+
} else if (this.costCoversEveryRound) {
|
|
160
|
+
this.summedCost = this.summedCost ? addCostBreakdowns(this.summedCost, roundCost) : roundCost;
|
|
161
|
+
if (!this.summedCost) this.costCoversEveryRound = false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.total;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Turn totals: summed tokens, plus the summed per-round cost when every round had rates. */
|
|
168
|
+
get total(): DetailedUsage {
|
|
169
|
+
return {
|
|
170
|
+
...this.tokens,
|
|
171
|
+
...(this.costCoversEveryRound && this.summedCost ? { estimatedCost: this.summedCost } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The model that served the LAST round, or undefined if no round named one. */
|
|
176
|
+
get lastServedModel(): string | undefined {
|
|
177
|
+
return this.lastServed;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** One entry per provider round, in order — the audit trail behind the summed total. */
|
|
181
|
+
get perRound(): TurnRoundUsage[] {
|
|
182
|
+
return this.rounds.map((round) => ({ model: round.model, usage: { ...round.usage } }));
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/yielding-stream.ts
CHANGED
|
@@ -70,6 +70,18 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
70
70
|
// Flag indicating the stream producer is done
|
|
71
71
|
private producerDone = false;
|
|
72
72
|
|
|
73
|
+
// Set when the CONSUMER leaves — `for await … break` (iterator.return) or
|
|
74
|
+
// iterator.throw. Nobody will read this stream again, so queued events are
|
|
75
|
+
// dropped rather than accumulated for a reader that is never coming back.
|
|
76
|
+
private consumerDeparted = false;
|
|
77
|
+
|
|
78
|
+
// Held so the listener can be removed at terminal: a long-lived caller
|
|
79
|
+
// signal shared across many streams otherwise accumulates one closure per
|
|
80
|
+
// stream, none of which are ever released.
|
|
81
|
+
private readonly onExternalAbort = () => {
|
|
82
|
+
this.cancel();
|
|
83
|
+
};
|
|
84
|
+
|
|
73
85
|
constructor(
|
|
74
86
|
private readonly options: YieldingStreamOptions,
|
|
75
87
|
private readonly runInference: (stream: YieldingStreamImpl) => Promise<void>
|
|
@@ -78,9 +90,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
78
90
|
|
|
79
91
|
// Link external signal if provided
|
|
80
92
|
if (options.signal) {
|
|
81
|
-
options.signal.addEventListener('abort',
|
|
82
|
-
this.cancel();
|
|
83
|
-
});
|
|
93
|
+
options.signal.addEventListener('abort', this.onExternalAbort, { once: true });
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -179,6 +189,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
179
189
|
this.emit({ type: 'aborted', reason: 'user' });
|
|
180
190
|
this.producerDone = true;
|
|
181
191
|
this.state = { status: 'done' };
|
|
192
|
+
this.releaseExternalSignal();
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
// ============================================================================
|
|
@@ -215,6 +226,27 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
215
226
|
await this.waitForEvent();
|
|
216
227
|
}
|
|
217
228
|
},
|
|
229
|
+
|
|
230
|
+
// `for await … break`, `return` out of the loop body, and a throw in
|
|
231
|
+
// the loop body all call return(). Without it the consumer walks away
|
|
232
|
+
// and the producer keeps streaming, resuming and billing. Cancelling
|
|
233
|
+
// here is the same act the consumer would perform by hand.
|
|
234
|
+
return: async (): Promise<IteratorResult<StreamEvent>> => {
|
|
235
|
+
this.departConsumer();
|
|
236
|
+
return { value: undefined as unknown as StreamEvent, done: true };
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// Reached through `yield*` delegation, or by a caller injecting an
|
|
240
|
+
// error by hand. Same departure as return() — nobody is reading this
|
|
241
|
+
// stream again — and then the error continues on its way, exactly as an
|
|
242
|
+
// async generator with no handler of its own would rethrow it. Reporting
|
|
243
|
+
// `done: true` here instead swallowed it: a `yield*` delegating to this
|
|
244
|
+
// stream resumed after the delegation as if nothing had been thrown in,
|
|
245
|
+
// and a direct `.throw(e)` resolved rather than rejecting.
|
|
246
|
+
throw: async (error?: unknown): Promise<IteratorResult<StreamEvent>> => {
|
|
247
|
+
this.departConsumer();
|
|
248
|
+
throw error;
|
|
249
|
+
},
|
|
218
250
|
};
|
|
219
251
|
}
|
|
220
252
|
|
|
@@ -226,6 +258,9 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
226
258
|
* Push an event to be yielded to the consumer.
|
|
227
259
|
*/
|
|
228
260
|
emit(event: StreamEvent): void {
|
|
261
|
+
// The consumer has left for good; queueing here would grow without bound
|
|
262
|
+
// for a reader that cannot observe it.
|
|
263
|
+
if (this.consumerDeparted) return;
|
|
229
264
|
this.eventQueue.push(event);
|
|
230
265
|
this.notifyEventWaiter();
|
|
231
266
|
}
|
|
@@ -255,6 +290,7 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
255
290
|
*/
|
|
256
291
|
markDone(): void {
|
|
257
292
|
this.producerDone = true;
|
|
293
|
+
this.releaseExternalSignal();
|
|
258
294
|
this.notifyEventWaiter();
|
|
259
295
|
}
|
|
260
296
|
|
|
@@ -269,6 +305,21 @@ export class YieldingStreamImpl implements YieldingStream {
|
|
|
269
305
|
// Private Helpers
|
|
270
306
|
// ============================================================================
|
|
271
307
|
|
|
308
|
+
/**
|
|
309
|
+
* The consumer has stopped iterating for good: drop everything queued for
|
|
310
|
+
* it, refuse further queueing, and cancel the producer.
|
|
311
|
+
*/
|
|
312
|
+
private departConsumer(): void {
|
|
313
|
+
this.consumerDeparted = true;
|
|
314
|
+
this.eventQueue.length = 0;
|
|
315
|
+
this.cancel();
|
|
316
|
+
this.eventQueue.length = 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private releaseExternalSignal(): void {
|
|
320
|
+
this.options.signal?.removeEventListener('abort', this.onExternalAbort);
|
|
321
|
+
}
|
|
322
|
+
|
|
272
323
|
private startInference(): void {
|
|
273
324
|
if (this.state.status !== 'idle') {
|
|
274
325
|
return; // Already started
|