-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllms-full.txt
More file actions
457 lines (363 loc) · 19.4 KB
/
Copy pathllms-full.txt
File metadata and controls
457 lines (363 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# yeet
> A tiny (~4.0 kB gzipped core; stream helpers on a separate ~2.9 kB subpath), zero-dependency `Either` / `Result` library for TypeScript. It models success and failure as ordinary values (`Right<A>` and `Left<E>`) and uses generator-based do-notation instead of a method-chaining API. You write normal control flow — `if`, `for`, early `return` — and TypeScript infers the error union from what you yield and raise. An optional build-time transform lowers the generators to plain branches for zero runtime overhead.
`yeet` deliberately has no `map` / `flatMap` / `andThen` / `pipe` surface. The happy path reads top-to-bottom; errors flow through the type system. Package name on npm is `@big-time/yeet`. It is ESM-only and ships TypeScript declarations.
## Install
```sh
npm install @big-time/yeet
# pnpm add @big-time/yeet
# yarn add @big-time/yeet
# bun add @big-time/yeet
```
```ts
import { either, left, right, type Either } from '@big-time/yeet'
```
## Mental model
An `Either<E, A>` is exactly one of:
```ts
left(error) // Left<E> — the failure branch
right(value) // Right<A> — the success branch
```
Inside an `either(...)` generator you unwrap a `Right` with `yield*`. If the value is a `Left`, the whole computation short-circuits and returns that `Left`. There is no unwrap-that-throws; failures stay as values.
```ts
const result = either(function* () {
const value = yield* right(42) // value is 42
yield* left('Nope') // short-circuits here; returns Left<'Nope'>
return value
}) // Either<'Nope', 42>
```
`return raise(error)` is the typed early-exit. `raise` is the first parameter of the generator function.
```ts
const result = either(function* (raise) {
const user = yield* getUser(id) // Left short-circuits
if (!user.active) return raise('Inactive') // typed early exit; narrows user.active below
return user
}) // Either<'UserNotFound' | 'Inactive', User>
```
The error union is inferred from the `Left`s you `yield*` and the values you `raise`. Do not annotate the generator body.
## Core API
```ts
left<E>(e: E): Left<E>
right<A>(a: A): Right<A>
isLeft<E, A>(v: Either<E, A>): v is Left<E>
isRight<E, A>(v: Either<E, A>): v is Right<A>
type Either<E, A> = Left<E> | Right<A>
```
`Left` and `Right` are small classes implementing `Symbol.iterator` (so they work with `yield*`), `toJSON`, and `Symbol.toPrimitive`. Tag checks (`value._tag === 'Left'`) work too.
## either — run a generator, short-circuit on the first Left
```ts
either(fn: (raise: RaiseContext) => Generator<Either<any, any>, Ret>): Either<E, A>
either(fn: (raise: RaiseContext) => AsyncGenerator<...>): Promise<Either<E, A>>
either(signal: AbortSignal, fn: (raise: AbortRaise, signal: ScopeSignal) => AsyncGenerator<...>): Promise<Either<Aborted | E, A>>
```
Sync:
```ts
const checkout = either(function* (raise) {
const session = yield* getSession('session-1')
if (!session.checkoutEnabled) return raise('CheckoutDisabled')
const user = yield* getUser(session.userId)
const cart = yield* getCart(user.id)
return { user, cart }
})
```
Async — await the `Either`, then `yield*` it:
```ts
const result = await either(async function* (raise) {
const user = yield* await fetchUser('1')
const orders = yield* await fetchOrders(user.id)
if (orders.length === 0) return raise('NoOrders')
return { user, orders }
})
```
Promises/thenables and operations that can throw on start go through `raise`, turning rejections and throws into `Left<Rejected>` instead of escaping as exceptions:
```ts
const result = await either(async function* (raise) {
const response = yield* await raise(fetch('/api/user'))
if (!response.ok) return raise({ _tag: 'HttpError', status: response.status })
const data = yield* await raise(() => response.json() as Promise<unknown>)
return data
})
// raise(fn) uses Promise.try, so a synchronous throw also becomes Left<Rejected>:
const config = yield* (await raise(() => JSON.parse(readConfigFile())))
```
Cancellation: pass an `AbortSignal` first. If it aborts mid-run, the result is `Left<Aborted>`.
The injected callback parameter is a callable `RaiseContext`. Prefer
`async function* ({ raise, signal })` when you need both, or
`async function* ({ signal })` when you only need cancellation. The same enriched
child signal is also passed as the callback's second argument for compatibility.
```ts
const result = await either(signal, async function* ({ signal }) {
const a = yield* await step1()
const b = yield* await step2(a, signal)
return b
})
```
`ScopeSignal` is a real `AbortSignal` plus scoped child work:
```ts
type ScopeSignal = AbortSignal & {
fork<E, A>(
task: (signal: ScopeSignal) => Either<E, A> | PromiseLike<Either<E, A>>,
): Promise<Exit<E, A>>
forkAll<const T extends readonly ScopeTask<any, any>[]>(
tasks: T,
): Promise<Exit<ScopeTaskError<T[number]>, ScopeTaskValues<T>>>
forkRace<const T extends readonly ScopeTask<any, any>[]>(
tasks: T,
): Promise<Exit<ScopeTaskError<T[number]>, ScopeTaskValue<T[number]>>>
}
type Exit<E, A> = Either<E | Rejected | Aborted | Suppressed, A>
```
Use `signal.fork(task)`, `signal.forkAll(tasks)`, and `signal.forkRace(tasks)` inside
async `either` for small structured-concurrency steps. Each task gets a child
signal. A child `Left`, a scoped `forkAll`/`forkRace` failure, a rejection, a
parent abort, or closing the outer generator aborts sibling work and waits for it to
settle. When a `forkRace` `Right` wins, losing tasks are aborted with
`siblingSettled()` (`{ _tag: 'SiblingSettled' }`).
If sibling teardown rejects while a scope is already unwinding, the primary
failure remains primary and cleanup rejections are attached as `Suppressed`
Exit-error data. If a `forkRace` `Right` wins but a loser rejects during abort
cleanup, the race returns `Left<Rejected>`.
Inside scoped child tasks, avoid returning a custom `Left<Cancelled>` solely
because `signal.aborted`; it widens task error unions and losing tasks are
discarded. Honor the signal and let yeet surface `Aborted` / `SiblingSettled`.
```ts
const result = await either(async function* ({ signal }) {
const user = signal.fork((signal) => fetchUser(id, signal))
const settings = signal.fork((signal) => fetchSettings(id, signal))
return {
user: yield* await user,
settings: yield* await settings,
}
})
// Promise<Either<Aborted | Rejected | FetchUserError | FetchSettingsError, { user: User; settings: Settings }>>
```
For clean inferred error unions, `yield* await` the fork promises you care
about. Runtime failures from detached forks still cancel the scope, but
TypeScript cannot infer an error type from a promise that is started and never
referenced.
Prefer `yield* await signal.forkAll([...])` for batches: it starts all child tasks,
preserves tuple order, and cancels siblings on first failure. Prefer
`yield* await signal.forkRace([...])` when the first child outcome wins; a winning
`Right` aborts losers with `SiblingSettled` without poisoning the enclosing
`either`.
## streams and bytes
Stream helpers are dependency-free and live on a separate subpath:
```ts
import {
bytes,
text,
json,
chunks,
consume,
collectText,
lines,
ndjson,
sse,
} from '@big-time/yeet/stream'
```
Use `bytes` / `text` / `json` when you want one final bounded value:
```ts
const result = await either(signal, async function* ({ signal }) {
const file = yield* await bytes(request, { maxBytes: 25_000_000, signal })
const doc = yield* await extractText(file)
return yield* await indexDocument(doc)
})
// Promise<Either<Aborted | StreamError | ExtractTextError | IndexDocumentError, IndexedDocument>>
```
Stream helpers compose with the build-time optimizer in non-abortable flows:
bounded steps like `yield* await json(body)` and structured item steps like
`for await (const next of ndjson(body)) { const item = yield* next }` lower to
plain `await`s, loops, and `Left` checks. Abortable `either(signal, ...)` still
stays on the runtime path today.
Use `collectText` for AI SDK-style text deltas when you need the final string.
It avoids allocating a `Right` for every successful chunk:
```ts
const result = await either(signal, async function* ({ signal }) {
return yield* await collectText(generation.textStream, {
tee: (delta) => writer.write(delta),
maxChars: 200_000,
signal,
error: providerError.promise,
})
})
// Promise<Either<Aborted | StreamError, string>>
```
Use `consume(source, { each, signal })` when you only need side effects while
draining a stream. `each` may return a `Left` to stop early; throws and rejected
callbacks become `Left<StreamConsumerError>`.
Use `chunks` / `lines` / `ndjson` / `sse` when each streamed item should compose
inside `either`:
```ts
const result = await either(signal, async function* ({ raise, signal }) {
const res = yield* await raise(fetch(url, { signal }))
for await (const next of sse(res.body, { signal })) {
const event = yield* next
if (event.event === 'error') return raise({ _tag: 'ProviderError', data: event.data })
yield* await handleProviderEvent(event)
}
return 'done' as const
})
// Promise<Either<Aborted | Rejected | StreamError | ProviderError | HandleProviderEventError, "done">>
```
```ts
const result = await either(signal, async function* ({ signal }) {
for await (const next of ndjson(toolResultStream, { maxBytes: 1_000_000, signal })) {
const event = yield* next
const valid = yield* validateToolEvent(event)
yield* await saveEvent(valid)
}
return 'ok' as const
})
// Promise<Either<Aborted | StreamError | ValidateToolEventError | SaveEventError, "ok">>
```
Cancellation is cooperative. Pass the same signal to `either(signal, ...)`, the
stream helper, and the underlying I/O operation. The driver can stop advancing
and close iterators, but it cannot interrupt a source that ignores the signal
and never settles.
Malformed NDJSON yields `Left<ParseError>` for that line and continues if the
consumer keeps iterating. Byte limits, line limits, invalid chunks, and decode
failures are stream-fatal.
Breaking a `chunks` / `lines` / `ndjson` / `sse` loop, or returning `Left` from
`consume(..., { each })`, cancels the underlying `ReadableStream`. Known stop
reasons are passed to `cancel(reason)`: abort `signal.reason`, external error
cause, or the fatal stream error.
## capture — get a Left as a value instead of short-circuiting
```ts
capture<E, A>(e: Either<E, A>): Right<Either<E, A>>
```
```ts
const result = either(function* (raise) {
const cached = yield* capture(getUserFromCache(id)) // does NOT short-circuit
if (cached._tag === 'Right') return cached.value
if (cached.error !== 'CacheMiss') return raise(cached.error)
return yield* getUserFromDatabase(id)
})
```
## validate — accumulate ALL errors
Use the injected `check` helper. Failures don't stop the run; every check executes. Returns `Left<E[]>` if any failed, else `Right<Ret>`. `check(...)` returns `undefined` inside the generator when its input was a `Left`.
```ts
validate(fn: (check: Check) => Generator<...>): Either<E[], Ret>
```
```ts
const result = validate(function* (check) {
const age = yield* check(validateAge(input.age)) // undefined if Left
const name = yield* check(validateName(input.name)) // still runs
return { age, name }
})
// Either<('TooYoung' | 'TooOld' | 'Empty' | 'TooLong')[], { age?: number; name?: string }>
```
## firstOf — first success wins
Tries yielded `Either`s in order; returns the first `Right`. If all fail, returns `Left<E[]>` with every error.
```ts
const user = firstOf(function* () {
yield getUserFromCache(id)
yield getUserFromReplica(id)
yield getUserFromPrimary(id)
}) // Either<Error[], User>
```
## collect — partition without short-circuiting
```ts
const { values, errors } = collect(function* () {
for (const item of items) yield processItem(item)
})
```
## all / collectAll — run several together
`all` accepts `Either`, `Promise<Either>`, or thunks returning either. Async inputs are observed concurrently. `all` returns the first `Left` by input order, or a tuple of all success values. `collectAll` never short-circuits and partitions into `{ values, errors }`.
```ts
const result = await either(async function* () {
const [user, settings] = yield* await all([fetchUser(id), fetchSettings(id)])
return { user, settings }
})
// tuple-typed:
const r = await all([right(1), Promise.resolve(right('two')), () => right(true)])
// Either<Rejected, [number, string, boolean]>
// wrap raw promises with raise so rejections become data:
const r2 = await either(async function* (raise) {
const [u, s] = yield* await all([raise(fetch('/api/user')), raise(fetch('/api/settings'))])
return { u, s }
})
const { values, errors } = await collectAll(ids.map((id) => () => fetchUser(id)))
```
## Guards
```ts
ensure<E>(cond: boolean, onFail: () => E): Either<E, void>
ensureNotNull<A, E>(value: A | null | undefined, onNull: () => E): Either<E, A>
```
```ts
const id = yield* ensureNotNull(input.userId, () => 'MissingUserId')
yield* ensure(id.length > 0, () => 'EmptyUserId')
```
## raise
```ts
raise<E>(e: E): Left<E> // typed early-exit value
raise<T>(p: PromiseLike<T>): Promise<Either<Rejected, T>> // rejection -> Left<Rejected>
raise<T>(fn: () => T | PromiseLike<T>): Promise<Either<Rejected, Awaited<T>>> // throw/reject -> Left<Rejected>
```
## Serialization & schemas
`Left`/`Right` serialize to tagged JSON. `toJSON()` eagerly converts nested values that provide their own `toJSON`, and turns native `Error`s into plain `{ name, message, ...fields }` (dropping `stack`).
```ts
JSON.stringify(left('Nope')) // {"_tag":"Left","error":"Nope"}
JSON.stringify(right({ id: 'user-1' })) // {"_tag":"Right","value":{"id":"user-1"}}
fromJSON(parsed) // rehydrate trusted serialized Either -> Left | Right
isSerializedEither(value) // detect the outer envelope only (does not validate payloads)
```
For untrusted JSON, pass Standard Schema-compatible validators (Zod, Valibot, ArkType, TypeBox, …). `yeet` imports none of them; it only looks for `~standard`.
```ts
import { eitherSchema, serializedEitherSchema, exitSchema, serializedExitSchema } from '@big-time/yeet'
const Serialized = serializedEitherSchema({ error: ApiError, value: User }) // -> transport shape
const Hydrated = eitherSchema({ error: ApiError, value: User }) // -> real Left | Right
const ExitJson = serializedExitSchema({ error: ApiError, value: User }) // -> Left<ApiError | Aborted | Rejected | Suppressed> | Right<User>
const ExitValue = exitSchema({ error: ApiError, value: User }) // -> hydrated Exit
```
Nested schemas are optional; without them the outer `{ _tag, error | value }` envelope is validated and the payload stays `unknown`.
For `Exit`, omitting the domain `error` schema accepts only yeet's built-in
`Aborted`, `Rejected`, and `Suppressed` error payloads.
## Lower-level: fold / foldAsync
Drive a generator yourself with a `Strategy`. Everything above is built on this. Most code never needs it.
```ts
type Strategy<Eff, Ret, Acc, R> = {
init: () => Acc
step: (eff: Eff, acc: Acc) => Step<Acc, R> // { done: true, result } | { done: false, send, acc }
finish: (ret: Ret, acc: Acc) => R
}
fold(fn: () => Generator<Eff, Ret>, strategy): R
foldAsync(gen: AsyncGenerator<Eff, Ret>, strategy): Promise<R>
```
## Build-time transform (optional, recommended for hot paths)
`yeet` ships an unplugin that lowers `either` / `validate` / `firstOf` / `collect` generators into plain branches at build time — `const x = yield* f()` becomes `const t = f(); if (t._tag === 'Left') return t; const x = t.value`. This removes generator allocation/resume/close overhead entirely. It is optional: code runs correctly without it; the transform only changes performance, not behavior.
```ts
// vite.config.ts
import yeet from '@big-time/yeet/unplugin'
export default { plugins: [yeet.vite({ /* moduleNames?: ['@big-time/yeet'] */ })] }
```
Bundler entrypoints: `yeet.vite`, `yeet.webpack`, `yeet.rollup`, `yeet.esbuild`, `yeet.rspack`, `yeet.bun` (also `yeet.raw` for the underlying transform).
A call is lowered only when it is a direct `either(function* () { ... })` literal whose delegated `yield*`s sit in statement position. Lowering is skipped (the runtime generator is used instead) when the generator references `this` or `arguments`, lets `raise` escape, uses non-delegated `yield`, or is stored in a variable rather than passed inline. These cases still work — they just keep the (slower) generator runtime.
## Idioms and pitfalls (for code generation)
- There is NO method-chain API. Do not generate `.map`, `.flatMap`, `.andThen`, `.mapErr`, `.pipe`. Use `either(function* () { ... })` with `yield*`.
- `yield* x` unwraps a `Right` to its value; a yielded `Left` short-circuits the enclosing runner.
- Use `return raise(error)` for typed early exit. `raise` is the generator's first parameter. `raise('X')` alone (without `return`) just builds a `Left`; you usually `return` it.
- Don't annotate the error type — it is inferred. Use `as const` on string-literal errors when you want a narrow literal type: `return raise('Inactive' as const)`.
- Async: `const x = yield* await fetchThing()` where `fetchThing` returns `Promise<Either>`. Wrap raw promises and throwing starts with `raise(...)` so rejections/throws become `Left<Rejected>`.
- Scoped child work: inside async `either`, destructure `{ signal }` and use `signal.fork((signal) => task(signal))`, `yield* await signal.forkAll([...])`, or `yield* await signal.forkRace([...])`; these keep child work under the current generator and cancel siblings on failures.
- Streams: import helpers from `@big-time/yeet/stream`, not the root. Use `collectText` / `consume` for hot text-delta drains, and `for await (const next of ndjson(...) | sse(...)) { const item = yield* next }` for structured streams.
- Optimizer: stream helpers in non-abortable `either(async function* ...)` compose with the transform (`yield* await json(...)`, `yield* await collectText(...)`, and `yield* next` where `next` is a `const` item from yeet `ndjson` / `sse` / `lines` / `chunks`). Abortable `either(signal, ...)` stays on the runtime driver.
- Check results with `isLeft` / `isRight` or `result._tag === 'Left'`.
- Choose the runner by intent: `either` (stop on first error), `validate` (collect all errors), `firstOf` (first success), `collect` / `collectAll` (partition), `all` (concurrent, first error or tuple), `capture` (treat a `Left` as a value).
- Avoid `this` / `arguments` inside `either` generators (also disables the build-time transform).
- ESM only; import from `@big-time/yeet`.
## API index
```
left right isLeft isRight
either capture validate firstOf collect all collectAll
ensure ensureNotNull raise siblingSettled suppressed
fromJSON isSerializedEither serializedEitherSchema eitherSchema
exitErrorSchema serializedExitSchema exitSchema
fold foldAsync
stream subpath: bytes text json chunks consume collectText lines ndjson sse
types: Either, Left, Right, Rejected, Aborted, SiblingSettled, Suppressed, Check, Raise, Strategy, Step,
SerializedEither, SerializedLeft, SerializedRight, Collected, InferE, InferA,
StreamError, StreamReadError, StreamExternalError, StreamConsumerError,
StreamTooLarge, TextTooLarge, LineTooLarge, InvalidChunk, DecodeError,
ParseError, ByteSource, StreamSource, SseEvent, RaiseContext, ScopeSignal,
ScopeTask, ScopeTaskError, ScopeTaskValue, ScopeTaskValues, AbortRaise,
Exit, ExitError, ExitErrorSchema, SerializedExitSchema, ExitSchema
```