@mlx-node/server 0.0.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/dist/endpoints/messages.d.ts +13 -0
- package/dist/endpoints/messages.d.ts.map +1 -0
- package/dist/endpoints/messages.js +511 -0
- package/dist/endpoints/models.d.ts +5 -0
- package/dist/endpoints/models.d.ts.map +1 -0
- package/dist/endpoints/models.js +10 -0
- package/dist/endpoints/responses.d.ts +79 -0
- package/dist/endpoints/responses.d.ts.map +1 -0
- package/dist/endpoints/responses.js +2816 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +84 -0
- package/dist/handler.d.ts +18 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +35 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/mappers/anthropic-request.d.ts +9 -0
- package/dist/mappers/anthropic-request.d.ts.map +1 -0
- package/dist/mappers/anthropic-request.js +241 -0
- package/dist/mappers/anthropic-response.d.ts +14 -0
- package/dist/mappers/anthropic-response.d.ts.map +1 -0
- package/dist/mappers/anthropic-response.js +112 -0
- package/dist/mappers/request.d.ts +18 -0
- package/dist/mappers/request.d.ts.map +1 -0
- package/dist/mappers/request.js +206 -0
- package/dist/mappers/response.d.ts +13 -0
- package/dist/mappers/response.d.ts.map +1 -0
- package/dist/mappers/response.js +116 -0
- package/dist/pending-writes.d.ts +337 -0
- package/dist/pending-writes.d.ts.map +1 -0
- package/dist/pending-writes.js +468 -0
- package/dist/registry.d.ts +363 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +497 -0
- package/dist/router.d.ts +6 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +78 -0
- package/dist/server.d.ts +80 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +158 -0
- package/dist/session-registry.d.ts +297 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +403 -0
- package/dist/streaming.d.ts +7 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +16 -0
- package/dist/tool-call-buffer.d.ts +26 -0
- package/dist/tool-call-buffer.d.ts.map +1 -0
- package/dist/tool-call-buffer.js +51 -0
- package/dist/transport-visibility.d.ts +56 -0
- package/dist/transport-visibility.d.ts.map +1 -0
- package/dist/transport-visibility.js +161 -0
- package/dist/types-anthropic.d.ts +144 -0
- package/dist/types-anthropic.d.ts.map +1 -0
- package/dist/types-anthropic.js +2 -0
- package/dist/types.d.ts +220 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +36 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** OpenAI Responses API request → internal `ChatMessage[]` + `ChatConfig`. */
|
|
2
|
+
function resolveContent(content) {
|
|
3
|
+
if (typeof content === 'string')
|
|
4
|
+
return content;
|
|
5
|
+
const parts = [];
|
|
6
|
+
for (const p of content) {
|
|
7
|
+
if (p.type === 'input_text') {
|
|
8
|
+
parts.push(p.text);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
throw new Error(`Unsupported content part type: "${p.type}"`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return parts.join('');
|
|
15
|
+
}
|
|
16
|
+
/** NAPI `ToolDefinition` requires `parameters.properties` to be a JSON string. */
|
|
17
|
+
function mapTool(tool) {
|
|
18
|
+
if (tool.type !== 'function') {
|
|
19
|
+
throw new Error(`Unsupported tool type: "${tool.type}"`);
|
|
20
|
+
}
|
|
21
|
+
const params = tool.parameters;
|
|
22
|
+
return {
|
|
23
|
+
type: 'function',
|
|
24
|
+
function: {
|
|
25
|
+
name: tool.name,
|
|
26
|
+
description: tool.description,
|
|
27
|
+
parameters: params
|
|
28
|
+
? {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: params['properties'] ? JSON.stringify(params['properties']) : undefined,
|
|
31
|
+
required: Array.isArray(params['required']) ? params['required'] : undefined,
|
|
32
|
+
}
|
|
33
|
+
: undefined,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function mapRequest(req, priorMessages) {
|
|
38
|
+
const messages = [];
|
|
39
|
+
if (req.instructions) {
|
|
40
|
+
messages.push({ role: 'system', content: req.instructions });
|
|
41
|
+
}
|
|
42
|
+
if (priorMessages) {
|
|
43
|
+
messages.push(...priorMessages);
|
|
44
|
+
}
|
|
45
|
+
// Coalesce a `message + function_call+` run (or a pure `function_call+` run)
|
|
46
|
+
// into ONE assistant `ChatMessage` carrying both `content` and `toolCalls`.
|
|
47
|
+
// `ChatSession.sendStream()` appends exactly one assistant message per turn,
|
|
48
|
+
// and `validateAndCanonicalizeHistoryToolOrder` requires each fan-out's
|
|
49
|
+
// `toolCalls` to pair 1:1 with the trailing tool block — splitting would
|
|
50
|
+
// reshape the conversation and make the walker reject the turn as orphaned.
|
|
51
|
+
// A `message` item immediately after a `function_call` starts a new turn.
|
|
52
|
+
if (typeof req.input === 'string') {
|
|
53
|
+
messages.push({ role: 'user', content: req.input });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
let prevItemType = null;
|
|
57
|
+
for (const item of req.input) {
|
|
58
|
+
if (item == null || typeof item !== 'object') {
|
|
59
|
+
throw new Error('Each input item must be a non-null object');
|
|
60
|
+
}
|
|
61
|
+
const itemType = item.type ?? 'message';
|
|
62
|
+
if (itemType === 'message') {
|
|
63
|
+
const msg = item;
|
|
64
|
+
// OpenAI "developer" maps to our "system".
|
|
65
|
+
const role = msg.role === 'developer' ? 'system' : msg.role;
|
|
66
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
67
|
+
throw new Error(`Unsupported message role: "${msg.role}"`);
|
|
68
|
+
}
|
|
69
|
+
messages.push({
|
|
70
|
+
role,
|
|
71
|
+
content: resolveContent(msg.content),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (itemType === 'function_call') {
|
|
75
|
+
// Coalesce onto the preceding assistant turn — see the loop header.
|
|
76
|
+
const fc = item;
|
|
77
|
+
const last = messages[messages.length - 1];
|
|
78
|
+
const canCoalesce = (prevItemType === 'function_call' || prevItemType === 'message') &&
|
|
79
|
+
last !== undefined &&
|
|
80
|
+
last.role === 'assistant';
|
|
81
|
+
if (canCoalesce) {
|
|
82
|
+
if (last.toolCalls === undefined) {
|
|
83
|
+
last.toolCalls = [];
|
|
84
|
+
}
|
|
85
|
+
last.toolCalls.push({ name: fc.name, arguments: fc.arguments, id: fc.call_id });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
messages.push({
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
content: '',
|
|
91
|
+
toolCalls: [{ name: fc.name, arguments: fc.arguments, id: fc.call_id }],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (itemType === 'function_call_output') {
|
|
96
|
+
const fco = item;
|
|
97
|
+
messages.push({
|
|
98
|
+
role: 'tool',
|
|
99
|
+
content: fco.output,
|
|
100
|
+
toolCallId: fco.call_id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error(`Unsupported input item type: "${itemType}"`);
|
|
105
|
+
}
|
|
106
|
+
prevItemType = itemType;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const config = {
|
|
110
|
+
reportPerformance: true,
|
|
111
|
+
};
|
|
112
|
+
if (req.max_output_tokens != null) {
|
|
113
|
+
config.maxNewTokens = req.max_output_tokens;
|
|
114
|
+
}
|
|
115
|
+
if (req.temperature != null) {
|
|
116
|
+
config.temperature = req.temperature;
|
|
117
|
+
}
|
|
118
|
+
if (req.top_p != null) {
|
|
119
|
+
config.topP = req.top_p;
|
|
120
|
+
}
|
|
121
|
+
if (req.reasoning?.effort) {
|
|
122
|
+
config.reasoningEffort = req.reasoning.effort;
|
|
123
|
+
}
|
|
124
|
+
if (req.tools && req.tools.length > 0) {
|
|
125
|
+
if (req.tool_choice === 'none') {
|
|
126
|
+
// Caller disabled tool use.
|
|
127
|
+
}
|
|
128
|
+
else if (typeof req.tool_choice === 'object' && req.tool_choice?.type === 'function') {
|
|
129
|
+
const targetName = req.tool_choice.name;
|
|
130
|
+
const matched = req.tools.filter((t) => t.name === targetName);
|
|
131
|
+
if (matched.length > 0) {
|
|
132
|
+
config.tools = matched.map(mapTool);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
config.tools = req.tools.map(mapTool);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (priorMessages && priorMessages.length > 0) {
|
|
140
|
+
config.reuseCache = true;
|
|
141
|
+
}
|
|
142
|
+
return { messages, config };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Reconstruct `ChatMessage[]` from a stored response chain. Each record
|
|
146
|
+
* stores `inputJson` (messages sent) and `outputJson` (output items); we
|
|
147
|
+
* interleave them.
|
|
148
|
+
*/
|
|
149
|
+
export function reconstructMessagesFromChain(chain) {
|
|
150
|
+
const messages = [];
|
|
151
|
+
for (const record of chain) {
|
|
152
|
+
const inputMessages = JSON.parse(record.inputJson);
|
|
153
|
+
messages.push(...inputMessages);
|
|
154
|
+
const outputItems = JSON.parse(record.outputJson);
|
|
155
|
+
let assistantText = '';
|
|
156
|
+
let thinkingText = '';
|
|
157
|
+
// Track presence vs. content separately: an empty-text `message` item
|
|
158
|
+
// still represents a real successful turn (the hot-path `ChatSession`
|
|
159
|
+
// always appends an assistant message per turn), so cold replay must
|
|
160
|
+
// preserve it or `primeHistory` will reshape the conversation.
|
|
161
|
+
let hadMessageItem = false;
|
|
162
|
+
let hadReasoningItem = false;
|
|
163
|
+
const toolCalls = [];
|
|
164
|
+
for (const item of outputItems) {
|
|
165
|
+
if (item.type === 'message') {
|
|
166
|
+
hadMessageItem = true;
|
|
167
|
+
if (item.content) {
|
|
168
|
+
assistantText += item.content.map((c) => c.text).join('');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (item.type === 'reasoning') {
|
|
172
|
+
hadReasoningItem = true;
|
|
173
|
+
if (item.summary) {
|
|
174
|
+
thinkingText += item.summary.map((s) => s.text).join('');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (item.type === 'function_call') {
|
|
178
|
+
toolCalls.push({
|
|
179
|
+
name: item.name,
|
|
180
|
+
arguments: item.arguments,
|
|
181
|
+
id: item.call_id,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Preserve the assistant turn whenever the record carried any assistant-facing
|
|
186
|
+
// item — message (even empty), reasoning, or function_call — because the hot-path
|
|
187
|
+
// `ChatSession` always appends one assistant message per completed turn.
|
|
188
|
+
// Keying on accumulated content would silently drop blank successful turns and
|
|
189
|
+
// reshape the replayed conversation. Records with no assistant items (input-only)
|
|
190
|
+
// are still skipped so we don't fabricate turns the live session never generated.
|
|
191
|
+
if (hadMessageItem || hadReasoningItem || toolCalls.length > 0) {
|
|
192
|
+
const assistantMsg = {
|
|
193
|
+
role: 'assistant',
|
|
194
|
+
content: assistantText,
|
|
195
|
+
};
|
|
196
|
+
if (thinkingText) {
|
|
197
|
+
assistantMsg.reasoningContent = thinkingText;
|
|
198
|
+
}
|
|
199
|
+
if (toolCalls.length > 0) {
|
|
200
|
+
assistantMsg.toolCalls = toolCalls;
|
|
201
|
+
}
|
|
202
|
+
messages.push(assistantMsg);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return messages;
|
|
206
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** ChatResult / ChatStreamEvent → OpenAI Responses API output. */
|
|
2
|
+
import type { ChatResult } from '@mlx-node/core';
|
|
3
|
+
import type { OutputItem, ResponseObject, ResponsesAPIRequest, ResponseUsage } from '../types.js';
|
|
4
|
+
export declare function genId(prefix: string): string;
|
|
5
|
+
export declare function mapFinishReasonToStatus(finishReason: string): 'completed' | 'incomplete';
|
|
6
|
+
export declare function buildOutputItems(result: ChatResult): OutputItem[];
|
|
7
|
+
export declare function buildUsage(result: ChatResult): ResponseUsage;
|
|
8
|
+
/** Concatenate all `output_text` parts from message items. */
|
|
9
|
+
export declare function computeOutputText(items: OutputItem[]): string;
|
|
10
|
+
export declare function buildResponseObject(result: ChatResult, req: ResponsesAPIRequest, responseId: string, previousResponseId?: string): ResponseObject;
|
|
11
|
+
/** Build an in-progress ResponseObject for `response.created` / `response.in_progress`, before any output exists. */
|
|
12
|
+
export declare function buildPartialResponse(req: ResponsesAPIRequest, responseId: string, previousResponseId?: string): ResponseObject;
|
|
13
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/mappers/response.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAIlE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAGV,UAAU,EAEV,cAAc,EACd,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAErB,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,YAAY,CAOxF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,EAAE,CAwCjE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAO5D;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAU7D;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,mBAAmB,EACxB,UAAU,EAAE,MAAM,EAClB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,cAAc,CAwBhB;AAED,qHAAqH;AACrH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,mBAAmB,EACxB,UAAU,EAAE,MAAM,EAClB,kBAAkB,CAAC,EAAE,MAAM,GAC1B,cAAc,CAqBhB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** ChatResult / ChatStreamEvent → OpenAI Responses API output. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
export function genId(prefix) {
|
|
4
|
+
return `${prefix}${randomUUID().replaceAll('-', '')}`;
|
|
5
|
+
}
|
|
6
|
+
export function mapFinishReasonToStatus(finishReason) {
|
|
7
|
+
switch (finishReason) {
|
|
8
|
+
case 'length':
|
|
9
|
+
return 'incomplete';
|
|
10
|
+
default:
|
|
11
|
+
return 'completed';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function buildOutputItems(result) {
|
|
15
|
+
const items = [];
|
|
16
|
+
if (result.thinking) {
|
|
17
|
+
const reasoningItem = {
|
|
18
|
+
id: genId('rs_'),
|
|
19
|
+
type: 'reasoning',
|
|
20
|
+
summary: [{ type: 'summary_text', text: result.thinking }],
|
|
21
|
+
};
|
|
22
|
+
items.push(reasoningItem);
|
|
23
|
+
}
|
|
24
|
+
const okToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
|
|
25
|
+
// Always emit a message item (possibly with empty text) unless there are tool calls and no text.
|
|
26
|
+
if (result.text || okToolCalls.length === 0) {
|
|
27
|
+
const messageItem = {
|
|
28
|
+
id: genId('msg_'),
|
|
29
|
+
type: 'message',
|
|
30
|
+
role: 'assistant',
|
|
31
|
+
status: mapFinishReasonToStatus(result.finishReason),
|
|
32
|
+
content: [{ type: 'output_text', text: result.text, annotations: [] }],
|
|
33
|
+
};
|
|
34
|
+
items.push(messageItem);
|
|
35
|
+
}
|
|
36
|
+
for (const tc of okToolCalls) {
|
|
37
|
+
const callId = tc.id ?? genId('call_');
|
|
38
|
+
const fcItem = {
|
|
39
|
+
id: genId('fc_'),
|
|
40
|
+
type: 'function_call',
|
|
41
|
+
call_id: callId,
|
|
42
|
+
name: tc.name,
|
|
43
|
+
arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
|
|
44
|
+
status: 'completed',
|
|
45
|
+
};
|
|
46
|
+
items.push(fcItem);
|
|
47
|
+
}
|
|
48
|
+
return items;
|
|
49
|
+
}
|
|
50
|
+
export function buildUsage(result) {
|
|
51
|
+
return {
|
|
52
|
+
input_tokens: result.promptTokens,
|
|
53
|
+
output_tokens: result.numTokens,
|
|
54
|
+
output_tokens_details: { reasoning_tokens: result.reasoningTokens },
|
|
55
|
+
total_tokens: result.promptTokens + result.numTokens,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Concatenate all `output_text` parts from message items. */
|
|
59
|
+
export function computeOutputText(items) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
if (item.type === 'message') {
|
|
63
|
+
for (const c of item.content) {
|
|
64
|
+
parts.push(c.text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return parts.join('');
|
|
69
|
+
}
|
|
70
|
+
export function buildResponseObject(result, req, responseId, previousResponseId) {
|
|
71
|
+
const output = buildOutputItems(result);
|
|
72
|
+
const status = mapFinishReasonToStatus(result.finishReason);
|
|
73
|
+
return {
|
|
74
|
+
id: responseId,
|
|
75
|
+
object: 'response',
|
|
76
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
77
|
+
status,
|
|
78
|
+
model: req.model,
|
|
79
|
+
output,
|
|
80
|
+
output_text: computeOutputText(output),
|
|
81
|
+
error: null,
|
|
82
|
+
incomplete_details: status === 'incomplete' ? { reason: 'max_output_tokens' } : null,
|
|
83
|
+
usage: buildUsage(result),
|
|
84
|
+
instructions: req.instructions ?? null,
|
|
85
|
+
temperature: req.temperature ?? null,
|
|
86
|
+
top_p: req.top_p ?? null,
|
|
87
|
+
max_output_tokens: req.max_output_tokens ?? null,
|
|
88
|
+
tools: req.tools ?? [],
|
|
89
|
+
tool_choice: req.tool_choice ?? null,
|
|
90
|
+
reasoning: req.reasoning ?? null,
|
|
91
|
+
previous_response_id: previousResponseId ?? null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Build an in-progress ResponseObject for `response.created` / `response.in_progress`, before any output exists. */
|
|
95
|
+
export function buildPartialResponse(req, responseId, previousResponseId) {
|
|
96
|
+
return {
|
|
97
|
+
id: responseId,
|
|
98
|
+
object: 'response',
|
|
99
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
100
|
+
status: 'in_progress',
|
|
101
|
+
model: req.model,
|
|
102
|
+
output: [],
|
|
103
|
+
output_text: '',
|
|
104
|
+
error: null,
|
|
105
|
+
incomplete_details: null,
|
|
106
|
+
usage: { input_tokens: 0, output_tokens: 0, output_tokens_details: { reasoning_tokens: 0 }, total_tokens: 0 },
|
|
107
|
+
instructions: req.instructions ?? null,
|
|
108
|
+
temperature: req.temperature ?? null,
|
|
109
|
+
top_p: req.top_p ?? null,
|
|
110
|
+
max_output_tokens: req.max_output_tokens ?? null,
|
|
111
|
+
tools: req.tools ?? [],
|
|
112
|
+
tool_choice: req.tool_choice ?? null,
|
|
113
|
+
reasoning: req.reasoning ?? null,
|
|
114
|
+
previous_response_id: previousResponseId ?? null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingResponseWrites — per-store in-memory index of response
|
|
3
|
+
* records whose `ResponseStore.store(...)` promise has been initiated
|
|
4
|
+
* but has not yet resolved.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* The responses endpoint starts `store.store(record)` synchronously
|
|
9
|
+
* inside the per-model `withExclusive` block (so the tracker observes
|
|
10
|
+
* the in-flight write before the mutex releases) but does NOT await
|
|
11
|
+
* it on the critical path. Without this tracker, a client that fires
|
|
12
|
+
* a follow-up request carrying `previous_response_id: A` immediately
|
|
13
|
+
* after seeing `response.completed` could race the off-lock
|
|
14
|
+
* `store.store()` for A and be rejected with a spurious
|
|
15
|
+
* `404 Previous response not found` because `getChain()` had not yet
|
|
16
|
+
* seen the row.
|
|
17
|
+
*
|
|
18
|
+
* The chain-lookup path consults `awaitPending(id)` BEFORE treating
|
|
19
|
+
* `getChain(id).length === 0` as a 404. If a write is still in flight,
|
|
20
|
+
* it awaits and retries `getChain`; the retry is guaranteed to see
|
|
21
|
+
* the row because the promise resolves only after the store's own
|
|
22
|
+
* serialization queue has accepted the insert.
|
|
23
|
+
*
|
|
24
|
+
* ## Semantics
|
|
25
|
+
*
|
|
26
|
+
* `track(id, promise)` registers `promise` under `id` and removes the
|
|
27
|
+
* entry when the promise settles (fulfill OR reject — a failed write
|
|
28
|
+
* leaves the tracker empty and the subsequent `getChain()` returns
|
|
29
|
+
* empty, which is the correct 404 shape).
|
|
30
|
+
*
|
|
31
|
+
* `awaitPending(id)` returns the tracked in-flight promise, or
|
|
32
|
+
* undefined. It is the SAME promise that was registered; our removal
|
|
33
|
+
* handler is attached via `.finally(...)` so the caller's rejection
|
|
34
|
+
* behaviour is unaffected. Callers typically swallow rejections with
|
|
35
|
+
* `await …catch(() => {})` before retrying `getChain()` because the
|
|
36
|
+
* rejection is already surfaced through the registering awaiter.
|
|
37
|
+
*
|
|
38
|
+
* ## Hard-timeout marker state
|
|
39
|
+
*
|
|
40
|
+
* When the responses-endpoint breaker decides a pending write is
|
|
41
|
+
* wedged it calls `markHardTimedOut(id, ttlMs, absoluteExpiresAt)`,
|
|
42
|
+
* which removes the id from `pending` (so the closure chain is
|
|
43
|
+
* reclaimable) and adds it to a lightweight `hardTimedOut` map. The
|
|
44
|
+
* continuation path then classifies a missing chain as retryable 503
|
|
45
|
+
* `storage_timeout` instead of permanent 404 while the marker is live.
|
|
46
|
+
*
|
|
47
|
+
* Marker lifetime is bounded by:
|
|
48
|
+
*
|
|
49
|
+
* min(write settlement, last continuation + TTL, absoluteExpiresAt)
|
|
50
|
+
*
|
|
51
|
+
* Five cleanup/refresh paths keep memory bounded and classification
|
|
52
|
+
* honest:
|
|
53
|
+
*
|
|
54
|
+
* 1. Fast path — `track()`'s `.finally(...)` deletes the marker as
|
|
55
|
+
* soon as the wedged write settles.
|
|
56
|
+
* 2. Read-refresh path — `isHardTimedOut(id)` slides `expiresAt`
|
|
57
|
+
* forward by `ttlMs` on every live hit (clamped at
|
|
58
|
+
* `absoluteExpiresAt`). Actively retried chains stay recoverable
|
|
59
|
+
* as long as the underlying write might still land.
|
|
60
|
+
* 3. Read-expire path — a full TTL elapse without refresh lazily
|
|
61
|
+
* deletes the entry and classifies the id as permanent 404.
|
|
62
|
+
* 4. Read-absolute-cap path — once `Date.now() >= absoluteExpiresAt`,
|
|
63
|
+
* the marker is deleted unconditionally. `ResponseStore.getChain()`
|
|
64
|
+
* hides the row past its own row TTL, so the retryable-503
|
|
65
|
+
* classification would be factually wrong.
|
|
66
|
+
* 5. Write-sweep path — `markHardTimedOut()` drains expired entries
|
|
67
|
+
* before inserting, bounded to `MAX_SWEEP_PER_INSERT` visits per
|
|
68
|
+
* call to keep the transition O(1) amortized even when the map
|
|
69
|
+
* is large. `isHardTimedOut()` moves refreshed entries to the
|
|
70
|
+
* Map tail so the bounded sweep cannot be starved by a stable
|
|
71
|
+
* head cohort of hot entries (natural LRU behaviour).
|
|
72
|
+
*
|
|
73
|
+
* The caller-side `absoluteExpiresAt` is the MINIMUM `expiresAt`
|
|
74
|
+
* across the whole resolved chain, not just the child record.
|
|
75
|
+
* `ResponseStore.getChain()` walks ancestors and aborts on the first
|
|
76
|
+
* expired link (see `crates/mlx-db/src/response_store/reader.rs:44-59`),
|
|
77
|
+
* so a child whose parent expires sooner is unrecoverable at the
|
|
78
|
+
* parent's expiry — not the child's. This module just receives the
|
|
79
|
+
* min-clamped value.
|
|
80
|
+
*
|
|
81
|
+
* ## Pending-entry earliest-expiry metadata
|
|
82
|
+
*
|
|
83
|
+
* The pre-breaker `awaitPending` timeout/probe path in `responses.ts`
|
|
84
|
+
* would otherwise classify any unresolved pending write as retryable
|
|
85
|
+
* `storage_timeout`, even when the resolved chain's earliest ancestor
|
|
86
|
+
* has already expired. A continuation whose parent is already past
|
|
87
|
+
* its row TTL cannot ever succeed via `getChain()`, so looping the
|
|
88
|
+
* client on 503 until the hard breaker fires is wasted.
|
|
89
|
+
*
|
|
90
|
+
* `track(id, promise, earliestExpiresAtMs?)` records the earliest
|
|
91
|
+
* recoverable expiry alongside the tracked promise in a per-id side
|
|
92
|
+
* map (`earliestExpiresByPending`). `getEarliestExpiresAtMs(id)`
|
|
93
|
+
* exposes it so the endpoint can short-circuit to 404 once
|
|
94
|
+
* `Date.now() >= earliestExpiresAtMs`. The side map is keyed
|
|
95
|
+
* identically to `pending` and cleared on the same `.finally(...)`
|
|
96
|
+
* hook — no extra lifecycle surface.
|
|
97
|
+
*
|
|
98
|
+
* ## Scope
|
|
99
|
+
*
|
|
100
|
+
* One tracker per `ResponseStore` instance is attached via a
|
|
101
|
+
* `WeakMap`, so callers never need to thread the tracker through the
|
|
102
|
+
* handler plumbing explicitly. This keeps the same (store, tracker)
|
|
103
|
+
* pair alive for the lifetime of the store and avoids leaks across
|
|
104
|
+
* test suites that recreate the store per describe block.
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Per-store tracker for in-flight `store.store(...)` writes.
|
|
108
|
+
*
|
|
109
|
+
* Thread-safety: Node.js is single-threaded within one event loop
|
|
110
|
+
* tick, so the internal `Map` is safe against concurrent mutation by
|
|
111
|
+
* design. Every mutation (`track`, `awaitPending`, `.finally(...)`
|
|
112
|
+
* cleanup) runs synchronously within a tick.
|
|
113
|
+
*/
|
|
114
|
+
export declare class PendingResponseWrites {
|
|
115
|
+
private readonly pending;
|
|
116
|
+
/**
|
|
117
|
+
* Per-pending-entry scalar recording the EARLIEST recoverable
|
|
118
|
+
* wall-clock expiry across (record + resolved ancestor chain) at
|
|
119
|
+
* `track()` time. Keyed identically to `pending` and cleared on
|
|
120
|
+
* the same `.finally(...)` settlement hook.
|
|
121
|
+
*
|
|
122
|
+
* The pre-breaker `awaitPending` timeout/probe path consults this
|
|
123
|
+
* via `getEarliestExpiresAtMs(id)`: once `Date.now()` has passed
|
|
124
|
+
* the earliest ancestor expiry, `getChain()` can never succeed,
|
|
125
|
+
* so the continuation short-circuits to 404 rather than looping
|
|
126
|
+
* the client on retryable 503.
|
|
127
|
+
*
|
|
128
|
+
* Optional: callers that pass `undefined` do not populate the
|
|
129
|
+
* side map; `getEarliestExpiresAtMs(id)` then returns `undefined`
|
|
130
|
+
* and the caller falls back to emitting retryable 503.
|
|
131
|
+
*/
|
|
132
|
+
private readonly earliestExpiresByPending;
|
|
133
|
+
/**
|
|
134
|
+
* Ids that crossed the hard-timeout breaker in `responses.ts`
|
|
135
|
+
* while their `store.store(...)` promise was still unresolved.
|
|
136
|
+
*
|
|
137
|
+
* Each entry records `{ expiresAt, ttlMs, absoluteExpiresAt }` in
|
|
138
|
+
* epoch-ms. `expiresAt` is the TTL-based sliding window;
|
|
139
|
+
* `absoluteExpiresAt` is the record row's own wall-clock expiry
|
|
140
|
+
* (`record.expiresAt * 1000`). See the module header for the full
|
|
141
|
+
* cleanup/refresh path inventory.
|
|
142
|
+
*
|
|
143
|
+
* Invariant: a marker is only meaningful for the SPECIFIC write
|
|
144
|
+
* that was live when `markHardTimedOut` was called. If a later
|
|
145
|
+
* `track(id, newPromise)` reuses the same id after a marker was
|
|
146
|
+
* set, the original promise's `.finally(...)` will still clear
|
|
147
|
+
* the marker on its settlement (clearing the wrong state for the
|
|
148
|
+
* new promise). In practice the responses endpoint scopes
|
|
149
|
+
* response ids to a single persist each, so this collision cannot
|
|
150
|
+
* arise.
|
|
151
|
+
*/
|
|
152
|
+
private readonly hardTimedOut;
|
|
153
|
+
/**
|
|
154
|
+
* Per-call visit budget for the opportunistic sweep invoked from
|
|
155
|
+
* `markHardTimedOut()`. Without a cap, refresh-on-read could keep
|
|
156
|
+
* the map arbitrarily large and every transition would pay O(N) on
|
|
157
|
+
* the main event loop (amortized O(N^2) across N wedged writes).
|
|
158
|
+
*
|
|
159
|
+
* Cap of 64 makes each transition O(1) with a small constant.
|
|
160
|
+
* JavaScript `Map` iterates in insertion order, so the sweep
|
|
161
|
+
* naturally drains the oldest markers first — which is where
|
|
162
|
+
* same-TTL expiries cluster. A backlog of K expired markers drains
|
|
163
|
+
* fully across ceil(K / 64) subsequent inserts, adequate because
|
|
164
|
+
* the read-path deletions are the authoritative cleanup signals
|
|
165
|
+
* for ids that actually receive continuation traffic.
|
|
166
|
+
*
|
|
167
|
+
* NOTE: the budget is a VISIT limit, not a delete limit. We stop
|
|
168
|
+
* after visiting MAX_SWEEP_PER_INSERT entries regardless of how
|
|
169
|
+
* many were expired, so cost stays bounded even when none of the
|
|
170
|
+
* first 64 entries are expired.
|
|
171
|
+
*/
|
|
172
|
+
private static readonly MAX_SWEEP_PER_INSERT;
|
|
173
|
+
/**
|
|
174
|
+
* Register an in-flight write under `id`. The caller must pass the
|
|
175
|
+
* raw `Promise<void>` returned by `store.store(record)` BEFORE
|
|
176
|
+
* awaiting it — otherwise the race window we are trying to close
|
|
177
|
+
* reopens.
|
|
178
|
+
*
|
|
179
|
+
* The tracker attaches its own `.finally(...)` handler to remove
|
|
180
|
+
* the entry when the promise settles. The caller's own handling of
|
|
181
|
+
* the promise (await / catch / log) is unaffected because
|
|
182
|
+
* `.finally` returns a new promise chain that does not steal the
|
|
183
|
+
* rejection.
|
|
184
|
+
*
|
|
185
|
+
* `earliestExpiresAtMs` is the EARLIEST wall-clock expiry (epoch-ms)
|
|
186
|
+
* across the record being persisted AND every resolved ancestor in
|
|
187
|
+
* its chain. When provided, it is stored in
|
|
188
|
+
* `earliestExpiresByPending` so the pre-breaker `awaitPending`
|
|
189
|
+
* timeout/probe path can short-circuit to 404 once
|
|
190
|
+
* `Date.now() >= earliestExpiresAtMs` rather than emit retryable
|
|
191
|
+
* 503 for a chain that cannot be recovered via `getChain()`.
|
|
192
|
+
* `Number.isFinite(...)` guards for rows lacking explicit
|
|
193
|
+
* `expiresAt`.
|
|
194
|
+
*/
|
|
195
|
+
track(id: string, writePromise: Promise<void>, earliestExpiresAtMs?: number): void;
|
|
196
|
+
/**
|
|
197
|
+
* Return the in-flight write promise for `id`, or `undefined` if
|
|
198
|
+
* none is tracked. Callers typically await with rejection
|
|
199
|
+
* suppressed (the tracker promise's rejection is already handled
|
|
200
|
+
* by the separate awaiter that initiated the write) and then
|
|
201
|
+
* retry `store.getChain(id)`.
|
|
202
|
+
*/
|
|
203
|
+
awaitPending(id: string): Promise<void> | undefined;
|
|
204
|
+
/**
|
|
205
|
+
* Return the EARLIEST wall-clock expiry (epoch-ms) captured
|
|
206
|
+
* alongside the in-flight write for `id` at `track()` time, or
|
|
207
|
+
* `undefined` if no pending write is tracked and no live marker
|
|
208
|
+
* covers this id.
|
|
209
|
+
*
|
|
210
|
+
* Consulted by the pre-breaker `awaitPending` timeout/probe path in
|
|
211
|
+
* `responses.ts` to distinguish a transient storage slowdown
|
|
212
|
+
* (retryable 503) from an unrecoverable chain where the earliest
|
|
213
|
+
* ancestor has already aged out (permanent 404).
|
|
214
|
+
*
|
|
215
|
+
* Falls back to the marker's `absoluteExpiresAt` when the pending
|
|
216
|
+
* entry has already been drained by `markHardTimedOut()` — otherwise
|
|
217
|
+
* a waiter that straddled the `pending -> hardTimedOut` transition
|
|
218
|
+
* would see `undefined` and fall through to retryable 503 for an
|
|
219
|
+
* unrecoverable chain. Both sites are fed
|
|
220
|
+
* `min(recordExpiresAtMs, chainEarliestExpiresAtMs)` by
|
|
221
|
+
* `initiatePersist` in `responses.ts`, so this is lossless.
|
|
222
|
+
*
|
|
223
|
+
* The fallback is gated on the shared `isMarkerLive` predicate so
|
|
224
|
+
* a marker whose TTL or absolute expiry has already passed cannot
|
|
225
|
+
* hand back a future scalar that contradicts `isHardTimedOut()`.
|
|
226
|
+
* Dead markers return `0` (sentinel meaning "already expired") —
|
|
227
|
+
* the consumer's `Date.now() >= earliestMs` guard always trips for
|
|
228
|
+
* `0`, producing a permanent 404 instead of falling through to the
|
|
229
|
+
* retryable-503 branch. This read path stays side-effect-free;
|
|
230
|
+
* `sweepExpired()` is the authoritative reaper.
|
|
231
|
+
*/
|
|
232
|
+
getEarliestExpiresAtMs(id: string): number | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* Shared, side-effect-free liveness predicate for hard-timeout
|
|
235
|
+
* markers: live iff `now < expiresAt` AND `now < absoluteExpiresAt`.
|
|
236
|
+
*
|
|
237
|
+
* Extracted so `getEarliestExpiresAtMs()` (read-only) and the
|
|
238
|
+
* mutating `isHardTimedOut()` poll agree on liveness without either
|
|
239
|
+
* invoking the other — `isHardTimedOut()` has refresh + move-to-tail
|
|
240
|
+
* side effects only correct for the polling-side caller.
|
|
241
|
+
*/
|
|
242
|
+
private static isMarkerLive;
|
|
243
|
+
/**
|
|
244
|
+
* Transition a pending entry to the hard-timed-out marker state.
|
|
245
|
+
*
|
|
246
|
+
* Called by the hard-timeout breaker in `responses.ts` when an
|
|
247
|
+
* in-flight `store.store(...)` has crossed the hard timeout and is
|
|
248
|
+
* presumed wedged. The `pending` entry is removed so `awaitPending`
|
|
249
|
+
* stops handing out the stale promise and the closure chain is
|
|
250
|
+
* reclaimable. The id is added to the marker map so the
|
|
251
|
+
* continuation path classifies missing chains as retryable 503
|
|
252
|
+
* `storage_timeout` instead of permanent 404 while the marker is
|
|
253
|
+
* live.
|
|
254
|
+
*
|
|
255
|
+
* `ttlMs` caps the marker lifetime independently of whether the
|
|
256
|
+
* underlying write settles. The caller (`responses.ts`) reads it
|
|
257
|
+
* from `MLX_HARD_TIMEOUT_MARKER_TTL_MS`; passing it in keeps this
|
|
258
|
+
* module env-free.
|
|
259
|
+
*
|
|
260
|
+
* `absoluteExpiresAt` is the response record's row expiry
|
|
261
|
+
* (`record.expiresAt * 1000`). The initial expiry is
|
|
262
|
+
* `min(Date.now() + ttlMs, absoluteExpiresAt)` so a short-lived
|
|
263
|
+
* record cannot have its marker outlive its row.
|
|
264
|
+
* `isHardTimedOut()` also consults `absoluteExpiresAt` on every
|
|
265
|
+
* read and hard-stops at that bound regardless of refreshes.
|
|
266
|
+
*
|
|
267
|
+
* Returns true if the id was an active pending entry and was moved
|
|
268
|
+
* to the marker; false if no pending entry existed at call time.
|
|
269
|
+
* A false return does NOT add the id to the marker — a marker
|
|
270
|
+
* without a backing promise has no fast cleanup signal (beyond
|
|
271
|
+
* TTL / absolute cap) and would produce spurious retryable-503
|
|
272
|
+
* signals in the meantime if the caller mis-routes ids.
|
|
273
|
+
*/
|
|
274
|
+
markHardTimedOut(id: string, ttlMs: number, absoluteExpiresAt: number): boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Drain expired marker entries (`expiresAt <= now` or
|
|
277
|
+
* `absoluteExpiresAt <= now`). Visits at most
|
|
278
|
+
* `MAX_SWEEP_PER_INSERT` entries per call so a caller cannot
|
|
279
|
+
* trigger an unbounded linear walk. `Map` insertion order makes
|
|
280
|
+
* this drain the oldest (most likely expired) markers first. The
|
|
281
|
+
* budget is a VISIT limit, not a delete limit.
|
|
282
|
+
*/
|
|
283
|
+
private sweepExpired;
|
|
284
|
+
/**
|
|
285
|
+
* Whether `id` is currently flagged as hard-timed-out. Used by the
|
|
286
|
+
* `previous_response_id` continuation path to classify a missing
|
|
287
|
+
* chain as retryable 503 `storage_timeout` vs. permanent 404.
|
|
288
|
+
*
|
|
289
|
+
* Read-path cleanup + refresh semantics:
|
|
290
|
+
*
|
|
291
|
+
* - Absolute cap is authoritative: once `now >= absoluteExpiresAt`
|
|
292
|
+
* the marker is deleted unconditionally. `ResponseStore.getChain()`
|
|
293
|
+
* hides the row past its own row TTL, so retryable-503 would
|
|
294
|
+
* lie to the client.
|
|
295
|
+
* - TTL-expired: lazy delete + return false.
|
|
296
|
+
* - Live hit: refresh `expiresAt = min(now + ttlMs, absoluteExpiresAt)`
|
|
297
|
+
* so actively-retried chains stay recoverable while the write
|
|
298
|
+
* might still land, without ever outliving the row.
|
|
299
|
+
* - On every live hit, move the entry to the Map tail (O(1)
|
|
300
|
+
* `delete` + `set` using insertion-order semantics). Without
|
|
301
|
+
* the rotation a stable head cohort of hot refreshed entries
|
|
302
|
+
* could indefinitely block the bounded `sweepExpired()` from
|
|
303
|
+
* reaching expired markers behind them. LRU rotation lets the
|
|
304
|
+
* sweep make forward progress.
|
|
305
|
+
*/
|
|
306
|
+
isHardTimedOut(id: string): boolean;
|
|
307
|
+
/** Number of writes currently in flight. Primarily for tests. */
|
|
308
|
+
get size(): number;
|
|
309
|
+
/**
|
|
310
|
+
* Number of ids currently in the hard-timed-out marker state.
|
|
311
|
+
* Primarily for tests. Delegates to the shared `sweepExpired()`
|
|
312
|
+
* helper so read-count and write-sweep stay in lockstep.
|
|
313
|
+
*
|
|
314
|
+
* Caveat: because the sweep is bounded (`MAX_SWEEP_PER_INSERT`
|
|
315
|
+
* visits per call), the reported size may include still-present
|
|
316
|
+
* expired entries that sit past the per-call visit budget.
|
|
317
|
+
* Callers needing exact reclaimed-count semantics should drive
|
|
318
|
+
* further `markHardTimedOut()` inserts (each drains another
|
|
319
|
+
* batch) or call `isHardTimedOut(id)` directly — the read-path
|
|
320
|
+
* deletion is authoritative and unbounded per-id.
|
|
321
|
+
*/
|
|
322
|
+
get hardTimedOutSize(): number;
|
|
323
|
+
/**
|
|
324
|
+
* Number of ids currently holding a scalar entry in the
|
|
325
|
+
* pending-side earliest-expiry map. Primarily for tests —
|
|
326
|
+
* regressions that need to validate the pending-side map is
|
|
327
|
+
* drained (independent of the marker-map fallback in
|
|
328
|
+
* `getEarliestExpiresAtMs`) require a direct readout.
|
|
329
|
+
*/
|
|
330
|
+
get earliestExpiresByPendingSize(): number;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Fetch (or lazily create) the tracker for a given store. Always
|
|
334
|
+
* returns the same tracker for the same store instance.
|
|
335
|
+
*/
|
|
336
|
+
export declare function getPendingWritesFor(store: object): PendingResponseWrites;
|
|
337
|
+
//# sourceMappingURL=pending-writes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-writes.d.ts","sourceRoot":"","sources":["../src/pending-writes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGG;AAEH;;;;;;;GAOG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IAEjE;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAkC;IAE3E;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CACjB;IAEZ;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAM;IAElD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI;IA0ClF;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAInD;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAWtD;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAI3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,OAAO;IAyB/E;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IAYpB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAqBnC,iEAAiE;IACjE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;;;;;;OAYG;IACH,IAAI,gBAAgB,IAAI,MAAM,CAG7B;IAED;;;;;;OAMG;IACH,IAAI,4BAA4B,IAAI,MAAM,CAEzC;CACF;AAYD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAOxE"}
|