@effect-uai/core 0.1.0 → 0.3.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/README.md +2 -2
- package/dist/{AiError-CqmYjXyx.d.mts → AiError-CBuPHVKA.d.mts} +1 -1
- package/dist/{AiError-CqmYjXyx.d.mts.map → AiError-CBuPHVKA.d.mts.map} +1 -1
- package/dist/Image-BZmKfIdq.d.mts +61 -0
- package/dist/Image-BZmKfIdq.d.mts.map +1 -0
- package/dist/{Items-D1C2686t.d.mts → Items-CB8Bo3FI.d.mts} +132 -80
- package/dist/Items-CB8Bo3FI.d.mts.map +1 -0
- package/dist/Media-D_CpcM1Z.d.mts +57 -0
- package/dist/Media-D_CpcM1Z.d.mts.map +1 -0
- package/dist/{StructuredFormat-B5ueioNr.d.mts → StructuredFormat-BWq5Hd1O.d.mts} +5 -5
- package/dist/StructuredFormat-BWq5Hd1O.d.mts.map +1 -0
- package/dist/{Tool-5wxOCuOh.d.mts → Tool-DjVufH7i.d.mts} +13 -13
- package/dist/Tool-DjVufH7i.d.mts.map +1 -0
- package/dist/{Turn-rlTfuHaQ.d.mts → Turn-OPaILVIB.d.mts} +12 -29
- package/dist/Turn-OPaILVIB.d.mts.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/dist-DV5ISja1.mjs +13782 -0
- package/dist/dist-DV5ISja1.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +1 -1
- package/dist/domain/AiError.mjs +1 -1
- package/dist/domain/Image.d.mts +2 -0
- package/dist/domain/Image.mjs +58 -0
- package/dist/domain/Image.mjs.map +1 -0
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -42
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -0
- package/dist/domain/Media.mjs +14 -0
- package/dist/domain/Media.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +2 -2
- package/dist/domain/Turn.mjs +12 -8
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +107 -0
- package/dist/embedding-model/Embedding.d.mts.map +1 -0
- package/dist/embedding-model/Embedding.mjs +18 -0
- package/dist/embedding-model/Embedding.mjs.map +1 -0
- package/dist/embedding-model/EmbeddingModel.d.mts +97 -0
- package/dist/embedding-model/EmbeddingModel.d.mts.map +1 -0
- package/dist/embedding-model/EmbeddingModel.mjs +17 -0
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -0
- package/dist/index.d.mts +16 -8
- package/dist/index.mjs +10 -2
- package/dist/language-model/LanguageModel.d.mts +12 -20
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +3 -20
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +111 -2
- package/dist/loop/Loop.d.mts.map +1 -0
- package/dist/loop/Loop.mjs +39 -6
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.d.mts +1 -0
- package/dist/loop/Loop.test.mjs +411 -0
- package/dist/loop/Loop.test.mjs.map +1 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs +1013 -0
- package/dist/magic-string.es-BgIV5Mu3.mjs.map +1 -0
- package/dist/math/Vector.d.mts +47 -0
- package/dist/math/Vector.d.mts.map +1 -0
- package/dist/math/Vector.mjs +117 -0
- package/dist/math/Vector.mjs.map +1 -0
- package/dist/observability/Metrics.d.mts +2 -2
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +1 -1
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/streaming/JSONL.mjs +1 -1
- package/dist/streaming/JSONL.test.d.mts +1 -0
- package/dist/streaming/JSONL.test.mjs +70 -0
- package/dist/streaming/JSONL.test.mjs.map +1 -0
- package/dist/streaming/Lines.mjs +1 -1
- package/dist/streaming/SSE.d.mts +2 -2
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +1 -1
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.d.mts +1 -0
- package/dist/streaming/SSE.test.mjs +72 -0
- package/dist/streaming/SSE.test.mjs.map +1 -0
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +6 -6
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +6 -3
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +7 -1
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Outcome.d.mts +138 -2
- package/dist/tool/Outcome.d.mts.map +1 -0
- package/dist/tool/Outcome.mjs +34 -18
- package/dist/tool/Outcome.mjs.map +1 -1
- package/dist/tool/Resolvers.d.mts +30 -25
- package/dist/tool/Resolvers.d.mts.map +1 -1
- package/dist/tool/Resolvers.mjs +54 -44
- package/dist/tool/Resolvers.mjs.map +1 -1
- package/dist/tool/Resolvers.test.d.mts +1 -0
- package/dist/tool/Resolvers.test.mjs +317 -0
- package/dist/tool/Resolvers.test.mjs.map +1 -0
- package/dist/tool/Tool.d.mts +1 -1
- package/dist/tool/Tool.mjs +1 -1
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +151 -2
- package/dist/tool/ToolEvent.d.mts.map +1 -0
- package/dist/tool/ToolEvent.mjs +30 -4
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +24 -15
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +14 -13
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.d.mts +1 -0
- package/dist/tool/Toolkit.test.mjs +113 -0
- package/dist/tool/Toolkit.test.mjs.map +1 -0
- package/package.json +29 -13
- package/src/domain/Image.ts +75 -0
- package/src/domain/Items.ts +18 -47
- package/src/domain/Media.ts +61 -0
- package/src/domain/Turn.ts +7 -17
- package/src/embedding-model/Embedding.ts +117 -0
- package/src/embedding-model/EmbeddingModel.ts +107 -0
- package/src/index.ts +9 -1
- package/src/language-model/LanguageModel.ts +2 -22
- package/src/loop/Loop.test.ts +114 -2
- package/src/loop/Loop.ts +69 -5
- package/src/math/Vector.ts +138 -0
- package/src/observability/Metrics.ts +1 -1
- package/src/streaming/SSE.ts +1 -1
- package/src/structured-format/StructuredFormat.ts +2 -2
- package/src/testing/MockProvider.ts +2 -2
- package/src/tool/HistoryCheck.ts +2 -5
- package/src/tool/Outcome.ts +39 -53
- package/src/tool/Resolvers.test.ts +46 -117
- package/src/tool/Resolvers.ts +74 -102
- package/src/tool/Tool.ts +9 -9
- package/src/tool/ToolEvent.ts +30 -26
- package/src/tool/Toolkit.test.ts +97 -2
- package/src/tool/Toolkit.ts +65 -67
- package/dist/Items-D1C2686t.d.mts.map +0 -1
- package/dist/Loop-CzSJo1h8.d.mts +0 -87
- package/dist/Loop-CzSJo1h8.d.mts.map +0 -1
- package/dist/Outcome-C2JYknCu.d.mts +0 -40
- package/dist/Outcome-C2JYknCu.d.mts.map +0 -1
- package/dist/StructuredFormat-B5ueioNr.d.mts.map +0 -1
- package/dist/Tool-5wxOCuOh.d.mts.map +0 -1
- package/dist/ToolEvent-B2N10hr3.d.mts +0 -29
- package/dist/ToolEvent-B2N10hr3.d.mts.map +0 -1
- package/dist/Turn-rlTfuHaQ.d.mts.map +0 -1
- package/dist/match/Match.d.mts +0 -16
- package/dist/match/Match.d.mts.map +0 -1
- package/dist/match/Match.mjs +0 -15
- package/dist/match/Match.mjs.map +0 -1
- package/src/match/Match.ts +0 -9
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { loop, loopWithState, next, nextAfter, stopAfter, stopEvent, value } from "./Loop.mjs";
|
|
2
|
+
import { i as it, n as globalExpect, r as describe } from "../dist-DV5ISja1.mjs";
|
|
3
|
+
import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef } from "effect";
|
|
4
|
+
//#region src/loop/Loop.test.ts
|
|
5
|
+
describe("Loop.loop", () => {
|
|
6
|
+
it("threads state across iterations and emits each iteration's substream in order", async () => {
|
|
7
|
+
const stream = loop(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n, n + .5]), n + 1));
|
|
8
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
9
|
+
0,
|
|
10
|
+
.5,
|
|
11
|
+
1,
|
|
12
|
+
1.5,
|
|
13
|
+
2,
|
|
14
|
+
2.5,
|
|
15
|
+
3
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
it("supports iterations that emit zero values and only decide", async () => {
|
|
19
|
+
const stream = loop(0, (n) => n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]));
|
|
20
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
it("supports Effect-returning bodies directly (no Stream.unwrap needed)", async () => {
|
|
23
|
+
const stream = loop(1, (n) => Effect.gen(function* () {
|
|
24
|
+
const doubled = yield* Effect.succeed(n * 2);
|
|
25
|
+
return doubled >= 16 ? stopAfter(Stream.fromIterable([doubled])) : nextAfter(Stream.fromIterable([doubled]), doubled);
|
|
26
|
+
}));
|
|
27
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
28
|
+
2,
|
|
29
|
+
4,
|
|
30
|
+
8,
|
|
31
|
+
16
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
it("still accepts Stream.unwrap-wrapped bodies for backward compatibility", async () => {
|
|
35
|
+
const stream = loop(1, (n) => Stream.unwrap(Effect.gen(function* () {
|
|
36
|
+
const doubled = yield* Effect.succeed(n * 2);
|
|
37
|
+
return doubled >= 4 ? stopAfter(Stream.fromIterable([doubled])) : nextAfter(Stream.fromIterable([doubled]), doubled);
|
|
38
|
+
})));
|
|
39
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([2, 4]);
|
|
40
|
+
});
|
|
41
|
+
it("propagates errors from the body's stream", async () => {
|
|
42
|
+
const boom = /* @__PURE__ */ new Error("boom");
|
|
43
|
+
const stream = loop(0, (n) => n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]));
|
|
44
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(stream)))._tag).toBe("Failure");
|
|
45
|
+
});
|
|
46
|
+
it("terminates silently if the body emits no Decision (mirrors paginate's silent stop)", async () => {
|
|
47
|
+
const stream = loop(0, (n) => Stream.fromIterable([value(n), value(n + 1)]));
|
|
48
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([0, 1]);
|
|
49
|
+
});
|
|
50
|
+
it("short-circuits the body's stream when a Decision is seen", async () => {
|
|
51
|
+
const stream = loop(0, (n) => n >= 2 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([
|
|
52
|
+
value(n),
|
|
53
|
+
next(n + 1),
|
|
54
|
+
value(n + 10)
|
|
55
|
+
]));
|
|
56
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(stream))).toEqual([
|
|
57
|
+
0,
|
|
58
|
+
1,
|
|
59
|
+
2
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it("is stack-safe and linear-time across many iterations", async () => {
|
|
63
|
+
const N = 1e5;
|
|
64
|
+
const stream = loop(0, (n) => n >= N ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([value(n), next(n + 1)]));
|
|
65
|
+
globalExpect(await Effect.runPromise(Stream.runFold(stream, () => 0, (acc) => acc + 1))).toBe(N + 1);
|
|
66
|
+
}, 1e4);
|
|
67
|
+
});
|
|
68
|
+
const scriptedModel = (script) => {
|
|
69
|
+
let i = 0;
|
|
70
|
+
return { streamTurn: () => {
|
|
71
|
+
const turn = script[i] ?? [];
|
|
72
|
+
i += 1;
|
|
73
|
+
return Stream.fromIterable(turn);
|
|
74
|
+
} };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Body factored out so both tests share it. Per iteration:
|
|
78
|
+
* 1. Stream the model's deltas; tap captures texts + tool calls into Refs.
|
|
79
|
+
* 2. flatMap projects deltas into UiEvents forwarded to the outer stream.
|
|
80
|
+
* 3. Continuation reads the captured calls; if any, runs them, emits
|
|
81
|
+
* tool_result events, builds the next state (with model swap if a tool
|
|
82
|
+
* asked for one), and emits `next(state)`. Otherwise `stop`.
|
|
83
|
+
*/
|
|
84
|
+
const conversationLoop = (initial, runTool) => loop(initial, (state) => Stream.unwrap(Effect.gen(function* () {
|
|
85
|
+
const textsRef = yield* Ref.make([]);
|
|
86
|
+
const toolCallsRef = yield* Ref.make([]);
|
|
87
|
+
const deltas = state.model.streamTurn(state.history).pipe(Stream.tap((d) => d.type === "text" ? Ref.update(textsRef, (t) => [...t, d.text]) : Ref.update(toolCallsRef, (t) => [...t, {
|
|
88
|
+
id: d.id,
|
|
89
|
+
name: d.name
|
|
90
|
+
}])), Stream.flatMap((d) => d.type === "text" ? Stream.fromIterable([value({
|
|
91
|
+
type: "text",
|
|
92
|
+
text: d.text
|
|
93
|
+
})]) : Stream.fromIterable([value({
|
|
94
|
+
type: "tool_started",
|
|
95
|
+
id: d.id,
|
|
96
|
+
name: d.name
|
|
97
|
+
})])));
|
|
98
|
+
const continuation = Stream.unwrap(Effect.gen(function* () {
|
|
99
|
+
const texts = yield* Ref.get(textsRef);
|
|
100
|
+
const toolCalls = yield* Ref.get(toolCallsRef);
|
|
101
|
+
if (toolCalls.length === 0) return stopAfter(Stream.empty);
|
|
102
|
+
const turnItems = [...texts.length > 0 ? [{
|
|
103
|
+
type: "assistant",
|
|
104
|
+
text: texts.join("")
|
|
105
|
+
}] : [], ...toolCalls.map((tc) => ({
|
|
106
|
+
type: "tool_call",
|
|
107
|
+
id: tc.id,
|
|
108
|
+
name: tc.name
|
|
109
|
+
}))];
|
|
110
|
+
const outcomes = toolCalls.map((call) => ({
|
|
111
|
+
call,
|
|
112
|
+
outcome: runTool(call)
|
|
113
|
+
}));
|
|
114
|
+
const events = outcomes.map(({ call, outcome }) => ({
|
|
115
|
+
type: "tool_result",
|
|
116
|
+
id: call.id,
|
|
117
|
+
output: outcome.output
|
|
118
|
+
}));
|
|
119
|
+
const resultItems = outcomes.map(({ call, outcome }) => ({
|
|
120
|
+
type: "tool_result",
|
|
121
|
+
id: call.id,
|
|
122
|
+
output: outcome.output
|
|
123
|
+
}));
|
|
124
|
+
const nextModel = outcomes.reduce((m, { outcome }) => outcome.nextModel ?? m, state.model);
|
|
125
|
+
const nextState = {
|
|
126
|
+
history: [
|
|
127
|
+
...state.history,
|
|
128
|
+
...turnItems,
|
|
129
|
+
...resultItems
|
|
130
|
+
],
|
|
131
|
+
model: nextModel
|
|
132
|
+
};
|
|
133
|
+
return nextAfter(Stream.fromIterable(events), nextState);
|
|
134
|
+
}));
|
|
135
|
+
return Stream.concat(deltas, continuation);
|
|
136
|
+
})));
|
|
137
|
+
describe("Loop.loop - LLM-style scenarios", () => {
|
|
138
|
+
it("forwards text deltas, tool start, tool result, and post-tool text in order", async () => {
|
|
139
|
+
const m = scriptedModel([[
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: "hello"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: " "
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: "world"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: "tool_call",
|
|
154
|
+
id: "c1",
|
|
155
|
+
name: "get_time"
|
|
156
|
+
}
|
|
157
|
+
], [{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: " time is "
|
|
160
|
+
}, {
|
|
161
|
+
type: "text",
|
|
162
|
+
text: "12:00"
|
|
163
|
+
}]]);
|
|
164
|
+
const runTool = (call) => ({ output: call.name === "get_time" ? "12:00" : "?" });
|
|
165
|
+
const initial = {
|
|
166
|
+
history: [{
|
|
167
|
+
type: "user",
|
|
168
|
+
text: "what time is it?"
|
|
169
|
+
}],
|
|
170
|
+
model: m
|
|
171
|
+
};
|
|
172
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))).toEqual([
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: "hello"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: " "
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: "world"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "tool_started",
|
|
187
|
+
id: "c1",
|
|
188
|
+
name: "get_time"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "tool_result",
|
|
192
|
+
id: "c1",
|
|
193
|
+
output: "12:00"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: " time is "
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: "12:00"
|
|
202
|
+
}
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
it("model swap mid-stream: m1 calls upgrade, m2 finishes the response", async () => {
|
|
206
|
+
const m2 = scriptedModel([[{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: "I am m2."
|
|
209
|
+
}, {
|
|
210
|
+
type: "text",
|
|
211
|
+
text: " The answer is 42."
|
|
212
|
+
}]]);
|
|
213
|
+
const m1 = scriptedModel([[
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: "Hard question."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: " Upgrading."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: "tool_call",
|
|
224
|
+
id: "u1",
|
|
225
|
+
name: "upgrade"
|
|
226
|
+
}
|
|
227
|
+
]]);
|
|
228
|
+
const runTool = (call) => call.name === "upgrade" ? {
|
|
229
|
+
output: "ok",
|
|
230
|
+
nextModel: m2
|
|
231
|
+
} : { output: "?" };
|
|
232
|
+
const initial = {
|
|
233
|
+
history: [{
|
|
234
|
+
type: "user",
|
|
235
|
+
text: "what is the meaning of life?"
|
|
236
|
+
}],
|
|
237
|
+
model: m1
|
|
238
|
+
};
|
|
239
|
+
globalExpect(await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))).toEqual([
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: "Hard question."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: " Upgrading."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: "tool_started",
|
|
250
|
+
id: "u1",
|
|
251
|
+
name: "upgrade"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "tool_result",
|
|
255
|
+
id: "u1",
|
|
256
|
+
output: "ok"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: "text",
|
|
260
|
+
text: "I am m2."
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: " The answer is 42."
|
|
265
|
+
}
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe("Loop.loop - pull-specific stream semantics", () => {
|
|
270
|
+
it("does not start the next iteration when downstream only takes the first value", async () => {
|
|
271
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
272
|
+
const callsRef = yield* Ref.make(0);
|
|
273
|
+
yield* loop(0, (n) => Stream.unwrap(Ref.update(callsRef, (calls) => calls + 1).pipe(Effect.as(n >= 10 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([value(n), next(n + 1)]))))).pipe(Stream.take(1), Stream.runCollect);
|
|
274
|
+
return yield* Ref.get(callsRef);
|
|
275
|
+
}))).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
it("propagates defects from the body instead of leaving the consumer waiting", async () => {
|
|
278
|
+
const defect = /* @__PURE__ */ new Error("defect");
|
|
279
|
+
const stream = loop(0, () => Stream.die(defect));
|
|
280
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(stream)))._tag).toBe("Failure");
|
|
281
|
+
});
|
|
282
|
+
it("runs body finalizers when a Decision short-circuits the body", async () => {
|
|
283
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
284
|
+
const releasesRef = yield* Ref.make([]);
|
|
285
|
+
const stream = loop(0, (n) => (n >= 1 ? Stream.fromIterable([value(n), stopEvent]) : Stream.fromIterable([
|
|
286
|
+
value(n),
|
|
287
|
+
next(n + 1),
|
|
288
|
+
value(n + 10)
|
|
289
|
+
])).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))));
|
|
290
|
+
globalExpect(yield* Stream.runCollect(stream)).toEqual([0, 1]);
|
|
291
|
+
return yield* Ref.get(releasesRef);
|
|
292
|
+
}))).toEqual([0, 1]);
|
|
293
|
+
});
|
|
294
|
+
it("runs the active body finalizer when the downstream consumer is interrupted", async () => {
|
|
295
|
+
globalExpect(await Effect.runPromise(Effect.gen(function* () {
|
|
296
|
+
const started = yield* Deferred.make();
|
|
297
|
+
const releasesRef = yield* Ref.make(0);
|
|
298
|
+
const body = () => Stream.concat(Stream.fromEffect(Deferred.succeed(started, void 0).pipe(Effect.as(value(0)))), Stream.never).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)));
|
|
299
|
+
const stream = loop(0, body);
|
|
300
|
+
const fiber = yield* Effect.forkChild(Stream.runCollect(stream));
|
|
301
|
+
yield* Deferred.await(started);
|
|
302
|
+
yield* Fiber.interrupt(fiber);
|
|
303
|
+
return yield* Ref.get(releasesRef);
|
|
304
|
+
}))).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
it("does not create a body scope if constructing the body stream defects", async () => {
|
|
307
|
+
const defect = /* @__PURE__ */ new Error("body construction failed");
|
|
308
|
+
globalExpect((await Effect.runPromiseExit(Stream.runCollect(loop(0, () => {
|
|
309
|
+
throw defect;
|
|
310
|
+
}))))._tag).toBe("Failure");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
describe("Loop.loopWithState", () => {
|
|
314
|
+
it("exposes the final state in the SubscriptionRef after the stream completes", async () => {
|
|
315
|
+
const program = Effect.gen(function* () {
|
|
316
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1));
|
|
317
|
+
const values = yield* Stream.runCollect(stream);
|
|
318
|
+
const finalState = yield* SubscriptionRef.get(state);
|
|
319
|
+
return {
|
|
320
|
+
values: Array.from(values),
|
|
321
|
+
finalState
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
const { values, finalState } = await Effect.runPromise(program);
|
|
325
|
+
globalExpect(values).toEqual([
|
|
326
|
+
0,
|
|
327
|
+
1,
|
|
328
|
+
2,
|
|
329
|
+
3
|
|
330
|
+
]);
|
|
331
|
+
globalExpect(finalState).toBe(3);
|
|
332
|
+
});
|
|
333
|
+
it("the state ref starts at `initial` and stays there if the loop stops without advancing", async () => {
|
|
334
|
+
const program = Effect.gen(function* () {
|
|
335
|
+
const { stream, state } = yield* loopWithState({ count: 7 }, () => Stream.fromIterable([stopEvent]));
|
|
336
|
+
yield* Stream.runDrain(stream);
|
|
337
|
+
return yield* SubscriptionRef.get(state);
|
|
338
|
+
});
|
|
339
|
+
globalExpect(await Effect.runPromise(program)).toEqual({ count: 7 });
|
|
340
|
+
});
|
|
341
|
+
it("a downstream consumer can read the live state between emitted values", async () => {
|
|
342
|
+
const program = Effect.gen(function* () {
|
|
343
|
+
const { stream, state } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1));
|
|
344
|
+
const seen = [];
|
|
345
|
+
yield* Stream.runForEach(stream, (v) => Effect.gen(function* () {
|
|
346
|
+
seen.push({
|
|
347
|
+
value: v,
|
|
348
|
+
stateAfter: yield* SubscriptionRef.get(state)
|
|
349
|
+
});
|
|
350
|
+
}));
|
|
351
|
+
return seen;
|
|
352
|
+
});
|
|
353
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
354
|
+
{
|
|
355
|
+
value: 0,
|
|
356
|
+
stateAfter: 0
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
value: 1,
|
|
360
|
+
stateAfter: 1
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
value: 2,
|
|
364
|
+
stateAfter: 2
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
value: 3,
|
|
368
|
+
stateAfter: 3
|
|
369
|
+
}
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
it("SubscriptionRef.changes emits every state transition to a concurrent observer", async () => {
|
|
373
|
+
const program = Effect.gen(function* () {
|
|
374
|
+
const start = yield* Latch.make(false);
|
|
375
|
+
const { stream, state } = yield* loopWithState(0, (n) => Effect.gen(function* () {
|
|
376
|
+
if (n === 0) yield* Latch.await(start);
|
|
377
|
+
return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1);
|
|
378
|
+
}));
|
|
379
|
+
const observerFiber = yield* Effect.forkChild(SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect));
|
|
380
|
+
yield* Effect.sleep("10 millis");
|
|
381
|
+
yield* Latch.open(start);
|
|
382
|
+
yield* Stream.runDrain(stream);
|
|
383
|
+
return Array.from(yield* Fiber.join(observerFiber));
|
|
384
|
+
});
|
|
385
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
386
|
+
0,
|
|
387
|
+
1,
|
|
388
|
+
2,
|
|
389
|
+
3
|
|
390
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
it("does not interfere with the body's value stream", async () => {
|
|
393
|
+
const program = Effect.gen(function* () {
|
|
394
|
+
const { stream } = yield* loopWithState(0, (n) => n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n, n + .5]), n + 1));
|
|
395
|
+
return Array.from(yield* Stream.runCollect(stream));
|
|
396
|
+
});
|
|
397
|
+
globalExpect(await Effect.runPromise(program)).toEqual([
|
|
398
|
+
0,
|
|
399
|
+
.5,
|
|
400
|
+
1,
|
|
401
|
+
1.5,
|
|
402
|
+
2,
|
|
403
|
+
2.5,
|
|
404
|
+
3
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
//#endregion
|
|
409
|
+
export {};
|
|
410
|
+
|
|
411
|
+
//# sourceMappingURL=Loop.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Loop.test.mjs","names":[],"sources":["../../src/loop/Loop.test.ts"],"sourcesContent":["import { Deferred, Effect, Fiber, Latch, Ref, Stream, SubscriptionRef } from \"effect\"\nimport { describe, expect, it } from \"vitest\"\nimport {\n type Event,\n loop,\n loopWithState,\n next,\n nextAfter,\n stopEvent,\n stopAfter,\n value,\n} from \"./Loop.js\"\n\ndescribe(\"Loop.loop\", () => {\n it(\"threads state across iterations and emits each iteration's substream in order\", async () => {\n // Each iter emits [n, n + 0.5] then continues; final iter emits [n] and stops.\n const stream = loop(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n\n it(\"supports iterations that emit zero values and only decide\", async () => {\n // Every iteration emits nothing, just bumps state; stops at 5.\n const stream = loop(0, (n: number) =>\n n >= 5 ? Stream.fromIterable([stopEvent]) : Stream.fromIterable([next(n + 1)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([])\n })\n\n it(\"supports Effect-returning bodies directly (no Stream.unwrap needed)\", async () => {\n // Each iter yields an Effect that doubles the state, then emits it.\n // Body returns Effect<Stream> directly; loop unwraps internally.\n const stream = loop(1, (n: number) =>\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 16\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4, 8, 16])\n })\n\n it(\"still accepts Stream.unwrap-wrapped bodies for backward compatibility\", async () => {\n const stream = loop(1, (n: number) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const doubled = yield* Effect.succeed(n * 2)\n return doubled >= 4\n ? stopAfter(Stream.fromIterable([doubled]))\n : nextAfter(Stream.fromIterable([doubled]), doubled)\n }),\n ),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([2, 4])\n })\n\n it(\"propagates errors from the body's stream\", async () => {\n const boom = new Error(\"boom\")\n const stream = loop(\n 0,\n (n: number): Stream.Stream<Event<number, number>, Error> =>\n n === 2 ? Stream.fail(boom) : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"terminates silently if the body emits no Decision (mirrors paginate's silent stop)\", async () => {\n // No decision emitted - loop just ends after the body's stream completes.\n const stream = loop(0, (n: number) => Stream.fromIterable([value(n), value(n + 1)]))\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1])\n })\n\n it(\"short-circuits the body's stream when a Decision is seen\", async () => {\n // Body emits [n, next(n+1), n+10]. Once the Decision is encountered, the\n // body's stream is interrupted - `n+10` is never pulled, so it never\n // flows to the outer stream. This is the correct behavior: a Decision\n // marks \"I'm done with this iteration\"; anything after it is dead code.\n const stream = loop(0, (n: number) =>\n n >= 2\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)]),\n )\n\n const result = await Effect.runPromise(Stream.runCollect(stream))\n expect(result).toEqual([0, 1, 2])\n })\n\n it(\"is stack-safe and linear-time across many iterations\", async () => {\n // 100k iterations far exceeds V8's typical stack depth (~10–15k frames).\n const N = 100_000\n const stream = loop(0, (n: number) =>\n n >= N\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n )\n\n const count = await Effect.runPromise(\n Stream.runFold(\n stream,\n (): number => 0,\n (acc) => acc + 1,\n ),\n )\n expect(count).toBe(N + 1) // 0..N inclusive\n }, 10_000)\n})\n\n// ---------------------------------------------------------------------------\n// Mock LLM scenario - proves the loop forwards deltas in real time and\n// correctly threads tool-result events between turns.\n// ---------------------------------------------------------------------------\n\ntype Delta =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n\ntype HistoryItem =\n | { readonly type: \"user\"; readonly text: string }\n | { readonly type: \"assistant\"; readonly text: string }\n | { readonly type: \"tool_call\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ntype UiEvent =\n | { readonly type: \"text\"; readonly text: string }\n | { readonly type: \"tool_started\"; readonly id: string; readonly name: string }\n | { readonly type: \"tool_result\"; readonly id: string; readonly output: string }\n\ninterface MockModel {\n readonly streamTurn: (history: ReadonlyArray<HistoryItem>) => Stream.Stream<Delta>\n}\n\ninterface State {\n readonly history: ReadonlyArray<HistoryItem>\n readonly model: MockModel\n}\n\ninterface ToolOutcome {\n readonly output: string\n readonly nextModel?: MockModel\n}\n\ntype ToolRunner = (call: { id: string; name: string }) => ToolOutcome\n\nconst scriptedModel = (script: ReadonlyArray<ReadonlyArray<Delta>>): MockModel => {\n let i = 0\n return {\n streamTurn: () => {\n const turn = script[i] ?? []\n i += 1\n return Stream.fromIterable(turn)\n },\n }\n}\n\n/**\n * Body factored out so both tests share it. Per iteration:\n * 1. Stream the model's deltas; tap captures texts + tool calls into Refs.\n * 2. flatMap projects deltas into UiEvents forwarded to the outer stream.\n * 3. Continuation reads the captured calls; if any, runs them, emits\n * tool_result events, builds the next state (with model swap if a tool\n * asked for one), and emits `next(state)`. Otherwise `stop`.\n */\nconst conversationLoop = (initial: State, runTool: ToolRunner) =>\n loop(initial, (state) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const textsRef = yield* Ref.make<ReadonlyArray<string>>([])\n const toolCallsRef = yield* Ref.make<\n ReadonlyArray<{ readonly id: string; readonly name: string }>\n >([])\n\n const deltas: Stream.Stream<Event<UiEvent, State>> = state.model\n .streamTurn(state.history)\n .pipe(\n Stream.tap((d) =>\n d.type === \"text\"\n ? Ref.update(textsRef, (t) => [...t, d.text])\n : Ref.update(toolCallsRef, (t) => [...t, { id: d.id, name: d.name }]),\n ),\n Stream.flatMap(\n (d): Stream.Stream<Event<UiEvent, State>> =>\n d.type === \"text\"\n ? Stream.fromIterable([value<UiEvent>({ type: \"text\", text: d.text })])\n : Stream.fromIterable([\n value<UiEvent>({ type: \"tool_started\", id: d.id, name: d.name }),\n ]),\n ),\n )\n\n const continuation: Stream.Stream<Event<UiEvent, State>> = Stream.unwrap(\n Effect.gen(function* () {\n const texts = yield* Ref.get(textsRef)\n const toolCalls = yield* Ref.get(toolCallsRef)\n\n if (toolCalls.length === 0) {\n return stopAfter(Stream.empty)\n }\n\n const turnItems: ReadonlyArray<HistoryItem> = [\n ...(texts.length > 0 ? [{ type: \"assistant\" as const, text: texts.join(\"\") }] : []),\n ...toolCalls.map(\n (tc): HistoryItem => ({ type: \"tool_call\", id: tc.id, name: tc.name }),\n ),\n ]\n\n const outcomes = toolCalls.map((call) => ({ call, outcome: runTool(call) }))\n\n const events: ReadonlyArray<UiEvent> = outcomes.map(({ call, outcome }) => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }))\n\n const resultItems: ReadonlyArray<HistoryItem> = outcomes.map(\n ({ call, outcome }): HistoryItem => ({\n type: \"tool_result\",\n id: call.id,\n output: outcome.output,\n }),\n )\n\n // Last requested model wins; default to the current one.\n const nextModel = outcomes.reduce(\n (m, { outcome }) => outcome.nextModel ?? m,\n state.model,\n )\n\n const nextState: State = {\n history: [...state.history, ...turnItems, ...resultItems],\n model: nextModel,\n }\n\n return nextAfter(Stream.fromIterable(events), nextState)\n }),\n )\n\n return Stream.concat(deltas, continuation)\n }),\n ),\n )\n\ndescribe(\"Loop.loop - LLM-style scenarios\", () => {\n it(\"forwards text deltas, tool start, tool result, and post-tool text in order\", async () => {\n const m = scriptedModel([\n [\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_call\", id: \"c1\", name: \"get_time\" },\n ],\n [\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) => ({\n output: call.name === \"get_time\" ? \"12:00\" : \"?\",\n })\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what time is it?\" }],\n model: m,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"hello\" },\n { type: \"text\", text: \" \" },\n { type: \"text\", text: \"world\" },\n { type: \"tool_started\", id: \"c1\", name: \"get_time\" },\n { type: \"tool_result\", id: \"c1\", output: \"12:00\" },\n { type: \"text\", text: \" time is \" },\n { type: \"text\", text: \"12:00\" },\n ])\n })\n\n it(\"model swap mid-stream: m1 calls upgrade, m2 finishes the response\", async () => {\n const m2 = scriptedModel([\n [\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ],\n ])\n\n const m1 = scriptedModel([\n [\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_call\", id: \"u1\", name: \"upgrade\" },\n ],\n ])\n\n const runTool: ToolRunner = (call) =>\n call.name === \"upgrade\" ? { output: \"ok\", nextModel: m2 } : { output: \"?\" }\n\n const initial: State = {\n history: [{ type: \"user\", text: \"what is the meaning of life?\" }],\n model: m1,\n }\n\n const events = await Effect.runPromise(Stream.runCollect(conversationLoop(initial, runTool)))\n\n expect(events).toEqual([\n { type: \"text\", text: \"Hard question.\" },\n { type: \"text\", text: \" Upgrading.\" },\n { type: \"tool_started\", id: \"u1\", name: \"upgrade\" },\n { type: \"tool_result\", id: \"u1\", output: \"ok\" },\n { type: \"text\", text: \"I am m2.\" },\n { type: \"text\", text: \" The answer is 42.\" },\n ])\n })\n})\n\ndescribe(\"Loop.loop - pull-specific stream semantics\", () => {\n it(\"does not start the next iteration when downstream only takes the first value\", async () => {\n const bodyCalls = await Effect.runPromise(\n Effect.gen(function* () {\n const callsRef = yield* Ref.make(0)\n const stream = loop(0, (n: number) =>\n Stream.unwrap(\n Ref.update(callsRef, (calls) => calls + 1).pipe(\n Effect.as(\n n >= 10\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1)]),\n ),\n ),\n ),\n )\n\n yield* stream.pipe(Stream.take(1), Stream.runCollect)\n return yield* Ref.get(callsRef)\n }),\n )\n\n expect(bodyCalls).toBe(1)\n })\n\n it(\"propagates defects from the body instead of leaving the consumer waiting\", async () => {\n const defect = new Error(\"defect\")\n const stream = loop(0, () => Stream.die(defect))\n\n const result = await Effect.runPromiseExit(Stream.runCollect(stream))\n\n expect(result._tag).toBe(\"Failure\")\n })\n\n it(\"runs body finalizers when a Decision short-circuits the body\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const releasesRef = yield* Ref.make<ReadonlyArray<number>>([])\n const stream = loop(0, (n: number) =>\n (n >= 1\n ? Stream.fromIterable([value(n), stopEvent])\n : Stream.fromIterable([value(n), next(n + 1), value(n + 10)])\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (values) => [...values, n]))),\n )\n\n const values = yield* Stream.runCollect(stream)\n expect(values).toEqual([0, 1])\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toEqual([0, 1])\n })\n\n it(\"runs the active body finalizer when the downstream consumer is interrupted\", async () => {\n const releases = await Effect.runPromise(\n Effect.gen(function* () {\n const started = yield* Deferred.make<void>()\n const releasesRef = yield* Ref.make(0)\n const body = (): Stream.Stream<Event<number, never>> =>\n Stream.concat(\n Stream.fromEffect(Deferred.succeed(started, undefined).pipe(Effect.as(value(0)))),\n Stream.never,\n ).pipe(Stream.ensuring(Ref.update(releasesRef, (n) => n + 1)))\n const stream = loop(0, body)\n\n const fiber = yield* Effect.forkChild(Stream.runCollect(stream))\n yield* Deferred.await(started)\n yield* Fiber.interrupt(fiber)\n\n return yield* Ref.get(releasesRef)\n }),\n )\n\n expect(releases).toBe(1)\n })\n\n it(\"does not create a body scope if constructing the body stream defects\", async () => {\n const defect = new Error(\"body construction failed\")\n const result = await Effect.runPromiseExit(\n Stream.runCollect(\n loop(0, (): Stream.Stream<Event<number, never>> => {\n throw defect\n }),\n ),\n )\n\n expect(result._tag).toBe(\"Failure\")\n })\n})\n\ndescribe(\"Loop.loopWithState\", () => {\n it(\"exposes the final state in the SubscriptionRef after the stream completes\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const values = yield* Stream.runCollect(stream)\n const finalState = yield* SubscriptionRef.get(state)\n return { values: Array.from(values), finalState }\n })\n\n const { values, finalState } = await Effect.runPromise(program)\n expect(values).toEqual([0, 1, 2, 3])\n // Last `next(state)` was `next(3)` before the iteration that emitted Stop.\n expect(finalState).toBe(3)\n })\n\n it(\"the state ref starts at `initial` and stays there if the loop stops without advancing\", async () => {\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState({ count: 7 }, () =>\n Stream.fromIterable([stopEvent]),\n )\n yield* Stream.runDrain(stream)\n return yield* SubscriptionRef.get(state)\n })\n\n expect(await Effect.runPromise(program)).toEqual({ count: 7 })\n })\n\n it(\"a downstream consumer can read the live state between emitted values\", async () => {\n // Body emits one value per iteration, then advances. A `Stream.runForEach`\n // consumer reads the ref each time a value arrives — proving the ref\n // tracks loop state without the body needing to surface it.\n const program = Effect.gen(function* () {\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n n >= 3 ? stopAfter(Stream.fromIterable([n])) : nextAfter(Stream.fromIterable([n]), n + 1),\n )\n const seen: Array<{ value: number; stateAfter: number }> = []\n yield* Stream.runForEach(stream, (v) =>\n Effect.gen(function* () {\n seen.push({ value: v, stateAfter: yield* SubscriptionRef.get(state) })\n }),\n )\n return seen\n })\n\n // For each iter `n`, the consumer reads the ref between values: it sees\n // the iteration's input state. The terminal iter (n=3) stops without\n // advancing, so its read still shows 3.\n expect(await Effect.runPromise(program)).toEqual([\n { value: 0, stateAfter: 0 },\n { value: 1, stateAfter: 1 },\n { value: 2, stateAfter: 2 },\n { value: 3, stateAfter: 3 },\n ])\n })\n\n it(\"SubscriptionRef.changes emits every state transition to a concurrent observer\", async () => {\n const program = Effect.gen(function* () {\n const start = yield* Latch.make(false)\n\n // Body waits on the latch in iter 0 so the observer can subscribe first.\n const { stream, state } = yield* loopWithState(0, (n: number) =>\n Effect.gen(function* () {\n if (n === 0) yield* Latch.await(start)\n return n >= 3 ? stopAfter(Stream.empty) : nextAfter(Stream.empty, n + 1)\n }),\n )\n\n // Fork the observer; take 4 distinct states (initial + 3 transitions).\n const observerFiber = yield* Effect.forkChild(\n SubscriptionRef.changes(state).pipe(Stream.take(4), Stream.runCollect),\n )\n\n // Give the observer fiber a chance to actually subscribe before the\n // loop starts advancing the ref. Without this, the loop could finish\n // before the observer's pubsub subscription is in place.\n yield* Effect.sleep(\"10 millis\")\n\n yield* Latch.open(start)\n yield* Stream.runDrain(stream)\n\n return Array.from(yield* Fiber.join(observerFiber))\n })\n\n // initial 0, then next(1), next(2), next(3) — four distinct states.\n expect(await Effect.runPromise(program)).toEqual([0, 1, 2, 3])\n })\n\n it(\"does not interfere with the body's value stream\", async () => {\n const program = Effect.gen(function* () {\n const { stream } = yield* loopWithState(0, (n: number) =>\n n >= 3\n ? stopAfter(Stream.fromIterable([n]))\n : nextAfter(Stream.fromIterable([n, n + 0.5]), n + 1),\n )\n return Array.from(yield* Stream.runCollect(stream))\n })\n\n expect(await Effect.runPromise(program)).toEqual([0, 0.5, 1, 1.5, 2, 2.5, 3])\n })\n})\n"],"mappings":";;;;AAaA,SAAS,mBAAmB;AAC1B,IAAG,iFAAiF,YAAY;EAE9F,MAAM,SAAS,KAAK,IAAI,MACtB,KAAK,IACD,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC,GACnC,UAAU,OAAO,aAAa,CAAC,GAAG,IAAI,GAAI,CAAC,EAAE,IAAI,EAAE,CACxD;AAGD,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ;GAAC;GAAG;GAAK;GAAG;GAAK;GAAG;GAAK;GAAE,CAAC;GACnD;AAEF,IAAG,6DAA6D,YAAY;EAE1E,MAAM,SAAS,KAAK,IAAI,MACtB,KAAK,IAAI,OAAO,aAAa,CAAC,UAAU,CAAC,GAAG,OAAO,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAC/E;AAGD,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ,EAAE,CAAC;GAC1B;AAEF,IAAG,uEAAuE,YAAY;EAGpF,MAAM,SAAS,KAAK,IAAI,MACtB,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ,IAAI,EAAE;AAC5C,UAAO,WAAW,KACd,UAAU,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC,GACzC,UAAU,OAAO,aAAa,CAAC,QAAQ,CAAC,EAAE,QAAQ;IACtD,CACH;AAGD,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAG,CAAC;GACrC;AAEF,IAAG,yEAAyE,YAAY;EACtF,MAAM,SAAS,KAAK,IAAI,MACtB,OAAO,OACL,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ,IAAI,EAAE;AAC5C,UAAO,WAAW,IACd,UAAU,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC,GACzC,UAAU,OAAO,aAAa,CAAC,QAAQ,CAAC,EAAE,QAAQ;IACtD,CACH,CACF;AAGD,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;GAC9B;AAEF,IAAG,4CAA4C,YAAY;EACzD,MAAM,uBAAO,IAAI,MAAM,OAAO;EAC9B,MAAM,SAAS,KACb,IACC,MACC,MAAM,IAAI,OAAO,KAAK,KAAK,GAAG,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,CAC7E;AAGD,gBAAO,MADc,OAAO,eAAe,OAAO,WAAW,OAAO,CAAC,EACvD,KAAK,CAAC,KAAK,UAAU;GACnC;AAEF,IAAG,sFAAsF,YAAY;EAEnG,MAAM,SAAS,KAAK,IAAI,MAAc,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;AAGpF,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;GAC9B;AAEF,IAAG,4DAA4D,YAAY;EAKzE,MAAM,SAAS,KAAK,IAAI,MACtB,KAAK,IACD,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,GAC1C,OAAO,aAAa;GAAC,MAAM,EAAE;GAAE,KAAK,IAAI,EAAE;GAAE,MAAM,IAAI,GAAG;GAAC,CAAC,CAChE;AAGD,eAAO,MADc,OAAO,WAAW,OAAO,WAAW,OAAO,CAAC,CACnD,CAAC,QAAQ;GAAC;GAAG;GAAG;GAAE,CAAC;GACjC;AAEF,IAAG,wDAAwD,YAAY;EAErE,MAAM,IAAI;EACV,MAAM,SAAS,KAAK,IAAI,MACtB,KAAK,IACD,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,GAC1C,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,CACjD;AASD,eAAO,MAPa,OAAO,WACzB,OAAO,QACL,cACc,IACb,QAAQ,MAAM,EAChB,CACF,CACY,CAAC,KAAK,IAAI,EAAE;IACxB,IAAO;EACV;AAsCF,MAAM,iBAAiB,WAA2D;CAChF,IAAI,IAAI;AACR,QAAO,EACL,kBAAkB;EAChB,MAAM,OAAO,OAAO,MAAM,EAAE;AAC5B,OAAK;AACL,SAAO,OAAO,aAAa,KAAK;IAEnC;;;;;;;;;;AAWH,MAAM,oBAAoB,SAAgB,YACxC,KAAK,UAAU,UACb,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,WAAW,OAAO,IAAI,KAA4B,EAAE,CAAC;CAC3D,MAAM,eAAe,OAAO,IAAI,KAE9B,EAAE,CAAC;CAEL,MAAM,SAA+C,MAAM,MACxD,WAAW,MAAM,QAAQ,CACzB,KACC,OAAO,KAAK,MACV,EAAE,SAAS,SACP,IAAI,OAAO,WAAW,MAAM,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,GAC3C,IAAI,OAAO,eAAe,MAAM,CAAC,GAAG,GAAG;EAAE,IAAI,EAAE;EAAI,MAAM,EAAE;EAAM,CAAC,CAAC,CACxE,EACD,OAAO,SACJ,MACC,EAAE,SAAS,SACP,OAAO,aAAa,CAAC,MAAe;EAAE,MAAM;EAAQ,MAAM,EAAE;EAAM,CAAC,CAAC,CAAC,GACrE,OAAO,aAAa,CAClB,MAAe;EAAE,MAAM;EAAgB,IAAI,EAAE;EAAI,MAAM,EAAE;EAAM,CAAC,CACjE,CAAC,CACT,CACF;CAEH,MAAM,eAAqD,OAAO,OAChE,OAAO,IAAI,aAAa;EACtB,MAAM,QAAQ,OAAO,IAAI,IAAI,SAAS;EACtC,MAAM,YAAY,OAAO,IAAI,IAAI,aAAa;AAE9C,MAAI,UAAU,WAAW,EACvB,QAAO,UAAU,OAAO,MAAM;EAGhC,MAAM,YAAwC,CAC5C,GAAI,MAAM,SAAS,IAAI,CAAC;GAAE,MAAM;GAAsB,MAAM,MAAM,KAAK,GAAG;GAAE,CAAC,GAAG,EAAE,EAClF,GAAG,UAAU,KACV,QAAqB;GAAE,MAAM;GAAa,IAAI,GAAG;GAAI,MAAM,GAAG;GAAM,EACtE,CACF;EAED,MAAM,WAAW,UAAU,KAAK,UAAU;GAAE;GAAM,SAAS,QAAQ,KAAK;GAAE,EAAE;EAE5E,MAAM,SAAiC,SAAS,KAAK,EAAE,MAAM,eAAe;GAC1E,MAAM;GACN,IAAI,KAAK;GACT,QAAQ,QAAQ;GACjB,EAAE;EAEH,MAAM,cAA0C,SAAS,KACtD,EAAE,MAAM,eAA4B;GACnC,MAAM;GACN,IAAI,KAAK;GACT,QAAQ,QAAQ;GACjB,EACF;EAGD,MAAM,YAAY,SAAS,QACxB,GAAG,EAAE,cAAc,QAAQ,aAAa,GACzC,MAAM,MACP;EAED,MAAM,YAAmB;GACvB,SAAS;IAAC,GAAG,MAAM;IAAS,GAAG;IAAW,GAAG;IAAY;GACzD,OAAO;GACR;AAED,SAAO,UAAU,OAAO,aAAa,OAAO,EAAE,UAAU;GACxD,CACH;AAED,QAAO,OAAO,OAAO,QAAQ,aAAa;EAC1C,CACH,CACF;AAEH,SAAS,yCAAyC;AAChD,IAAG,8EAA8E,YAAY;EAC3F,MAAM,IAAI,cAAc,CACtB;GACE;IAAE,MAAM;IAAQ,MAAM;IAAS;GAC/B;IAAE,MAAM;IAAQ,MAAM;IAAK;GAC3B;IAAE,MAAM;IAAQ,MAAM;IAAS;GAC/B;IAAE,MAAM;IAAa,IAAI;IAAM,MAAM;IAAY;GAClD,EACD,CACE;GAAE,MAAM;GAAQ,MAAM;GAAa,EACnC;GAAE,MAAM;GAAQ,MAAM;GAAS,CAChC,CACF,CAAC;EAEF,MAAM,WAAuB,UAAU,EACrC,QAAQ,KAAK,SAAS,aAAa,UAAU,KAC9C;EAED,MAAM,UAAiB;GACrB,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM;IAAoB,CAAC;GACrD,OAAO;GACR;AAID,eAAO,MAFc,OAAO,WAAW,OAAO,WAAW,iBAAiB,SAAS,QAAQ,CAAC,CAAC,CAE/E,CAAC,QAAQ;GACrB;IAAE,MAAM;IAAQ,MAAM;IAAS;GAC/B;IAAE,MAAM;IAAQ,MAAM;IAAK;GAC3B;IAAE,MAAM;IAAQ,MAAM;IAAS;GAC/B;IAAE,MAAM;IAAgB,IAAI;IAAM,MAAM;IAAY;GACpD;IAAE,MAAM;IAAe,IAAI;IAAM,QAAQ;IAAS;GAClD;IAAE,MAAM;IAAQ,MAAM;IAAa;GACnC;IAAE,MAAM;IAAQ,MAAM;IAAS;GAChC,CAAC;GACF;AAEF,IAAG,qEAAqE,YAAY;EAClF,MAAM,KAAK,cAAc,CACvB,CACE;GAAE,MAAM;GAAQ,MAAM;GAAY,EAClC;GAAE,MAAM;GAAQ,MAAM;GAAsB,CAC7C,CACF,CAAC;EAEF,MAAM,KAAK,cAAc,CACvB;GACE;IAAE,MAAM;IAAQ,MAAM;IAAkB;GACxC;IAAE,MAAM;IAAQ,MAAM;IAAe;GACrC;IAAE,MAAM;IAAa,IAAI;IAAM,MAAM;IAAW;GACjD,CACF,CAAC;EAEF,MAAM,WAAuB,SAC3B,KAAK,SAAS,YAAY;GAAE,QAAQ;GAAM,WAAW;GAAI,GAAG,EAAE,QAAQ,KAAK;EAE7E,MAAM,UAAiB;GACrB,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM;IAAgC,CAAC;GACjE,OAAO;GACR;AAID,eAAO,MAFc,OAAO,WAAW,OAAO,WAAW,iBAAiB,SAAS,QAAQ,CAAC,CAAC,CAE/E,CAAC,QAAQ;GACrB;IAAE,MAAM;IAAQ,MAAM;IAAkB;GACxC;IAAE,MAAM;IAAQ,MAAM;IAAe;GACrC;IAAE,MAAM;IAAgB,IAAI;IAAM,MAAM;IAAW;GACnD;IAAE,MAAM;IAAe,IAAI;IAAM,QAAQ;IAAM;GAC/C;IAAE,MAAM;IAAQ,MAAM;IAAY;GAClC;IAAE,MAAM;IAAQ,MAAM;IAAsB;GAC7C,CAAC;GACF;EACF;AAEF,SAAS,oDAAoD;AAC3D,IAAG,gFAAgF,YAAY;AAqB7F,eAAO,MApBiB,OAAO,WAC7B,OAAO,IAAI,aAAa;GACtB,MAAM,WAAW,OAAO,IAAI,KAAK,EAAE;AAanC,UAZe,KAAK,IAAI,MACtB,OAAO,OACL,IAAI,OAAO,WAAW,UAAU,QAAQ,EAAE,CAAC,KACzC,OAAO,GACL,KAAK,KACD,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,GAC1C,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,CACjD,CACF,CACF,CAGU,CAAC,KAAK,OAAO,KAAK,EAAE,EAAE,OAAO,WAAW;AACrD,UAAO,OAAO,IAAI,IAAI,SAAS;IAC/B,CACH,CAEgB,CAAC,KAAK,EAAE;GACzB;AAEF,IAAG,4EAA4E,YAAY;EACzF,MAAM,yBAAS,IAAI,MAAM,SAAS;EAClC,MAAM,SAAS,KAAK,SAAS,OAAO,IAAI,OAAO,CAAC;AAIhD,gBAAO,MAFc,OAAO,eAAe,OAAO,WAAW,OAAO,CAAC,EAEvD,KAAK,CAAC,KAAK,UAAU;GACnC;AAEF,IAAG,gEAAgE,YAAY;AAiB7E,eAAO,MAhBgB,OAAO,WAC5B,OAAO,IAAI,aAAa;GACtB,MAAM,cAAc,OAAO,IAAI,KAA4B,EAAE,CAAC;GAC9D,MAAM,SAAS,KAAK,IAAI,OACrB,KAAK,IACF,OAAO,aAAa,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,GAC1C,OAAO,aAAa;IAAC,MAAM,EAAE;IAAE,KAAK,IAAI,EAAE;IAAE,MAAM,IAAI,GAAG;IAAC,CAAC,EAC7D,KAAK,OAAO,SAAS,IAAI,OAAO,cAAc,WAAW,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,CAC7E;AAGD,gBAAO,OADe,OAAO,WAAW,OAAO,CACjC,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;AAC9B,UAAO,OAAO,IAAI,IAAI,YAAY;IAClC,CACH,CAEe,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;GAChC;AAEF,IAAG,8EAA8E,YAAY;AAoB3F,eAAO,MAnBgB,OAAO,WAC5B,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,SAAS,MAAY;GAC5C,MAAM,cAAc,OAAO,IAAI,KAAK,EAAE;GACtC,MAAM,aACJ,OAAO,OACL,OAAO,WAAW,SAAS,QAAQ,SAAS,KAAA,EAAU,CAAC,KAAK,OAAO,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC,EACjF,OAAO,MACR,CAAC,KAAK,OAAO,SAAS,IAAI,OAAO,cAAc,MAAM,IAAI,EAAE,CAAC,CAAC;GAChE,MAAM,SAAS,KAAK,GAAG,KAAK;GAE5B,MAAM,QAAQ,OAAO,OAAO,UAAU,OAAO,WAAW,OAAO,CAAC;AAChE,UAAO,SAAS,MAAM,QAAQ;AAC9B,UAAO,MAAM,UAAU,MAAM;AAE7B,UAAO,OAAO,IAAI,IAAI,YAAY;IAClC,CACH,CAEe,CAAC,KAAK,EAAE;GACxB;AAEF,IAAG,wEAAwE,YAAY;EACrF,MAAM,yBAAS,IAAI,MAAM,2BAA2B;AASpD,gBAAO,MARc,OAAO,eAC1B,OAAO,WACL,KAAK,SAA8C;AACjD,SAAM;IACN,CACH,CACF,EAEa,KAAK,CAAC,KAAK,UAAU;GACnC;EACF;AAEF,SAAS,4BAA4B;AACnC,IAAG,6EAA6E,YAAY;EAC1F,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,EAAE,QAAQ,UAAU,OAAO,cAAc,IAAI,MACjD,KAAK,IAAI,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAC1F;GACD,MAAM,SAAS,OAAO,OAAO,WAAW,OAAO;GAC/C,MAAM,aAAa,OAAO,gBAAgB,IAAI,MAAM;AACpD,UAAO;IAAE,QAAQ,MAAM,KAAK,OAAO;IAAE;IAAY;IACjD;EAEF,MAAM,EAAE,QAAQ,eAAe,MAAM,OAAO,WAAW,QAAQ;AAC/D,eAAO,OAAO,CAAC,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC;AAEpC,eAAO,WAAW,CAAC,KAAK,EAAE;GAC1B;AAEF,IAAG,yFAAyF,YAAY;EACtG,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,EAAE,QAAQ,UAAU,OAAO,cAAc,EAAE,OAAO,GAAG,QACzD,OAAO,aAAa,CAAC,UAAU,CAAC,CACjC;AACD,UAAO,OAAO,SAAS,OAAO;AAC9B,UAAO,OAAO,gBAAgB,IAAI,MAAM;IACxC;AAEF,eAAO,MAAM,OAAO,WAAW,QAAQ,CAAC,CAAC,QAAQ,EAAE,OAAO,GAAG,CAAC;GAC9D;AAEF,IAAG,wEAAwE,YAAY;EAIrF,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,EAAE,QAAQ,UAAU,OAAO,cAAc,IAAI,MACjD,KAAK,IAAI,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAC1F;GACD,MAAM,OAAqD,EAAE;AAC7D,UAAO,OAAO,WAAW,SAAS,MAChC,OAAO,IAAI,aAAa;AACtB,SAAK,KAAK;KAAE,OAAO;KAAG,YAAY,OAAO,gBAAgB,IAAI,MAAM;KAAE,CAAC;KACtE,CACH;AACD,UAAO;IACP;AAKF,eAAO,MAAM,OAAO,WAAW,QAAQ,CAAC,CAAC,QAAQ;GAC/C;IAAE,OAAO;IAAG,YAAY;IAAG;GAC3B;IAAE,OAAO;IAAG,YAAY;IAAG;GAC3B;IAAE,OAAO;IAAG,YAAY;IAAG;GAC3B;IAAE,OAAO;IAAG,YAAY;IAAG;GAC5B,CAAC;GACF;AAEF,IAAG,iFAAiF,YAAY;EAC9F,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,QAAQ,OAAO,MAAM,KAAK,MAAM;GAGtC,MAAM,EAAE,QAAQ,UAAU,OAAO,cAAc,IAAI,MACjD,OAAO,IAAI,aAAa;AACtB,QAAI,MAAM,EAAG,QAAO,MAAM,MAAM,MAAM;AACtC,WAAO,KAAK,IAAI,UAAU,OAAO,MAAM,GAAG,UAAU,OAAO,OAAO,IAAI,EAAE;KACxE,CACH;GAGD,MAAM,gBAAgB,OAAO,OAAO,UAClC,gBAAgB,QAAQ,MAAM,CAAC,KAAK,OAAO,KAAK,EAAE,EAAE,OAAO,WAAW,CACvE;AAKD,UAAO,OAAO,MAAM,YAAY;AAEhC,UAAO,MAAM,KAAK,MAAM;AACxB,UAAO,OAAO,SAAS,OAAO;AAE9B,UAAO,MAAM,KAAK,OAAO,MAAM,KAAK,cAAc,CAAC;IACnD;AAGF,eAAO,MAAM,OAAO,WAAW,QAAQ,CAAC,CAAC,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC;GAC9D;AAEF,IAAG,mDAAmD,YAAY;EAChE,MAAM,UAAU,OAAO,IAAI,aAAa;GACtC,MAAM,EAAE,WAAW,OAAO,cAAc,IAAI,MAC1C,KAAK,IACD,UAAU,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC,GACnC,UAAU,OAAO,aAAa,CAAC,GAAG,IAAI,GAAI,CAAC,EAAE,IAAI,EAAE,CACxD;AACD,UAAO,MAAM,KAAK,OAAO,OAAO,WAAW,OAAO,CAAC;IACnD;AAEF,eAAO,MAAM,OAAO,WAAW,QAAQ,CAAC,CAAC,QAAQ;GAAC;GAAG;GAAK;GAAG;GAAK;GAAG;GAAK;GAAE,CAAC;GAC7E;EACF"}
|