xantiagoma 0.2.0 → 0.2.1

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 CHANGED
@@ -22,6 +22,8 @@
22
22
  npm install xantiagoma
23
23
  ```
24
24
 
25
+ Release process notes live in [docs/RELEASING.md](./docs/RELEASING.md).
26
+
25
27
  ## Entry Points
26
28
 
27
29
  | Import | Description | Dependencies |
@@ -172,6 +174,37 @@ Cursor tokens are produced by a two-stage codec (`serializer` + `encoder`, both
172
174
  | `fetchWithProgress` | Fetch with upload/download progress | [src](./src/fetch-with-progress.ts) | [tests](./test/fetch-with-progress.test.ts) |
173
175
  | `createHttpInterceptor` | Intercept fetch + XHR with rules | [src](./src/intercept-http.ts) | [tests](./test/intercept-http.test.tsx) |
174
176
 
177
+ ## Sonner Utilities (`xantiagoma/sonner`)
178
+
179
+ Toast helpers for streaming iterables/generators through [Sonner](https://sonner.emilkowal.ski/).
180
+
181
+ | Export | Description | Source | Tests |
182
+ | ------------------ | ----------------------------------------------------------------------- | ---------------------------- | ------------------------------------ |
183
+ | `toastStream` | Blocking/awaitable stream toast; resolves to `{ items, returnValue }` | [src](./src/toast-stream.ts) | [tests](./test/toast-stream.test.ts) |
184
+ | `toastStreamAsync` | Non-blocking stream toast; returns toast id immediately with `unwrap()` | [src](./src/toast-stream.ts) | [tests](./test/toast-stream.test.ts) |
185
+
186
+ Use `toastStream` when the caller should wait for completion:
187
+
188
+ ```ts
189
+ import { toastStream } from "xantiagoma/sonner";
190
+
191
+ const { items, returnValue } = await toastStream(source, {
192
+ loading: "Loading...",
193
+ streaming: ({ count }) => `Received ${count}`,
194
+ success: ({ count }) => `Done: ${count} items`,
195
+ });
196
+ ```
197
+
198
+ Use `toastStreamAsync` when the caller should continue immediately, matching
199
+ Sonner's `toast.promise(...).unwrap()` style:
200
+
201
+ ```ts
202
+ import { toastStreamAsync } from "xantiagoma/sonner";
203
+
204
+ const toastId = toastStreamAsync(source, { loading: "Loading..." });
205
+ const { items, returnValue } = await toastId.unwrap();
206
+ ```
207
+
175
208
  ## React Utilities (`xantiagoma/react`)
176
209
 
177
210
  | Export | Description | Source | Tests |
@@ -204,7 +204,64 @@ async function toastStream(source, data = {}) {
204
204
  returnValue: result.returnValue
205
205
  };
206
206
  }
207
+ function toastStreamAsync(source, data = {}) {
208
+ const { loading, delay, ...rest } = data;
209
+ const delayConfig = normalizeDelay(delay);
210
+ const needsAsyncResolve = typeof loading === "function" || typeof loading === "object" && loading !== null && "message" in loading;
211
+ const initialLoadingMsg = needsAsyncResolve ? "Processing..." : loading ?? "Processing...";
212
+ const toastId = sonner.toast.loading(initialLoadingMsg);
213
+ const promise = (async () => {
214
+ const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;
215
+ if (needsAsyncResolve) {
216
+ const { message: loadingMsg, options: loadingOptions } = await resolveLoading(loading, "Processing...");
217
+ sonner.toast.loading(loadingMsg, {
218
+ id: toastId,
219
+ ...loadingOptions
220
+ });
221
+ }
222
+ const result = await consumeStream({
223
+ source,
224
+ toastId,
225
+ streaming,
226
+ onItem,
227
+ delayConfig
228
+ });
229
+ await onFinally?.();
230
+ if (delayConfig.result) await require_src.wait(delayConfig.result);
231
+ if (!result.ok) {
232
+ const partial = {
233
+ count: result.items.length,
234
+ items: result.items
235
+ };
236
+ const { message: errorMsg, options: errorOptions } = await resolveErrorResult(error, result.error, partial, "Stream failed");
237
+ sonner.toast.error(errorMsg, {
238
+ id: toastId,
239
+ ...toastOptions,
240
+ ...errorOptions
241
+ });
242
+ throw result.error;
243
+ }
244
+ const { message: successMsg, options: successOptions } = await resolveResult(success, {
245
+ count: result.items.length,
246
+ items: result.items,
247
+ returnValue: result.returnValue
248
+ }, `Complete! ${result.items.length} items`);
249
+ sonner.toast.success(successMsg, {
250
+ id: toastId,
251
+ ...toastOptions,
252
+ ...successOptions
253
+ });
254
+ return {
255
+ items: result.items,
256
+ returnValue: result.returnValue
257
+ };
258
+ })();
259
+ const res = toastId;
260
+ res.unwrap = () => promise;
261
+ return res;
262
+ }
207
263
  //#endregion
208
264
  exports.toastStream = toastStream;
265
+ exports.toastStreamAsync = toastStreamAsync;
209
266
 
210
267
  //# sourceMappingURL=entry-sonner.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"entry-sonner.cjs","names":["resolveStreamSource","wait","isGenerator","isAsyncGenerator","AsyncChannel","isAsyncIterable","isIterable","toAsyncIterable","toast"],"sources":["../src/toast-stream.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport {\n AsyncChannel,\n isAsyncGenerator,\n isAsyncIterable,\n isGenerator,\n isIterable,\n resolveStreamSource,\n type StreamSource,\n toAsyncIterable,\n wait,\n} from \"./index.ts\";\nimport { type ExternalToast, toast } from \"sonner\";\n\n// Types matching Sonner's patterns\ntype StreamExternalToast = Omit<ExternalToast, \"description\">;\n\ninterface StreamExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\n/** Result can be string, ReactNode, or callback returning either (sync or async) */\ntype StreamResultT<TData = unknown> =\n | string\n | ReactNode\n | ((data: TData) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended result includes full toast options */\ntype StreamExtendedResultT<TData = unknown> =\n | StreamExtendedResult\n | ((data: TData) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Error result - callback receives error first, then partial data */\ntype StreamErrorResultT<TYield> =\n | string\n | ReactNode\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended error result with toast options */\ntype StreamExtendedErrorResultT<TYield> =\n | StreamExtendedResult\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Streaming progress data passed to callbacks */\ntype StreamingData<T> = {\n count: number;\n latest: T;\n /** All items received so far (including latest) */\n items: T[];\n};\n\n/** Success data passed to callbacks - includes return value if generator has one */\ntype SuccessData<TYield, TReturn = void> = {\n count: number;\n items: TYield[];\n /** The return value from the generator (if any) */\n returnValue: TReturn;\n};\n\n/** Partial data available when an error occurs */\ntype ErrorPartialData<TYield> = {\n /** Number of items received before error */\n count: number;\n /** Items received before error */\n items: TYield[];\n};\n\n/** Result of consuming a stream */\ntype StreamResult<TYield, TReturn> = {\n items: TYield[];\n returnValue: TReturn;\n};\n\n/** Internal result from consumeStream - includes partial data on error */\ntype ConsumeResult<TYield, TReturn> =\n | { ok: true; items: TYield[]; returnValue: TReturn }\n | { ok: false; items: TYield[]; error: unknown };\n\n/** Granular delay configuration */\ntype DelayConfig = {\n /** Delay between items in ms */\n items?: number;\n /** Delay after loading, before first item */\n first?: number;\n /** Delay before showing success/error */\n result?: number;\n};\n\n/** Loading result - no data, just static or callback */\ntype LoadingResultT = string | ReactNode | (() => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended loading result with toast options */\ninterface LoadingExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\ntype LoadingExtendedResultT =\n | LoadingExtendedResult\n | (() => LoadingExtendedResult | Promise<LoadingExtendedResult>);\n\n/** Options matching Sonner's PromiseData pattern */\ntype ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {\n /**\n * Message shown while waiting for first item.\n * Supports string, ReactNode, callback, or extended result with toast options.\n */\n loading?: LoadingResultT | LoadingExtendedResultT;\n /** Message shown while streaming - updates on each item */\n streaming?: StreamResultT<StreamingData<TYield>> | StreamExtendedResultT<StreamingData<TYield>>;\n /** Message shown on successful completion */\n success?:\n | StreamResultT<SuccessData<TYield, TReturn>>\n | StreamExtendedResultT<SuccessData<TYield, TReturn>>;\n /** Message shown on error - callback receives (error, { count, items }) */\n error?: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield>;\n /** Called when stream ends (success or error) */\n finally?: () => void | Promise<void>;\n /** Called for each item received */\n onItem?: (item: TYield, count: number) => void;\n /**\n * Artificial delay for visualizing sync streams.\n * - `number`: applies to all phases (first, items, result)\n * - `object`: granular control over individual phases\n *\n * @example\n * // Simple: 500ms for all phases (first, between items, before result)\n * delay: 500\n *\n * @example\n * // Granular: different delays for different phases\n * delay: { first: 500, items: 300, result: 200 }\n */\n delay?: number | DelayConfig;\n};\n\n/** Resolve a result that can be string, ReactNode, or callback */\nasync function resolveResult<TData>(\n result: StreamResultT<TData> | StreamExtendedResultT<TData> | undefined,\n data: TData,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(data);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve an error result that receives (error, partialData) */\nasync function resolveErrorResult<TYield>(\n result: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield> | undefined,\n error: unknown,\n partial: ErrorPartialData<TYield>,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(error, partial);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve loading result - no data passed to callback */\nasync function resolveLoading(\n result: LoadingResultT | LoadingExtendedResultT | undefined,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result();\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as LoadingExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as LoadingExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Normalize delay option to full DelayConfig */\nfunction normalizeDelay(delay: number | DelayConfig | undefined): DelayConfig {\n if (delay === undefined) {\n return {};\n }\n if (typeof delay === \"number\") {\n // Simple number applies to all phases: first, items, and result\n return { first: delay, items: delay, result: delay };\n }\n return delay;\n}\n\n/** Options for consumeStream */\ntype ConsumeStreamOptions<TYield, TReturn> = {\n source: StreamSource<TYield>;\n toastId: string | number;\n streaming: ToastStreamData<TYield, TReturn>[\"streaming\"];\n onItem: ToastStreamData<TYield, TReturn>[\"onItem\"];\n delayConfig: DelayConfig;\n};\n\n/** Core streaming logic - consumes source and updates toast, captures return value */\nasync function consumeStream<TYield, TReturn>(\n options: ConsumeStreamOptions<TYield, TReturn>,\n): Promise<ConsumeResult<TYield, TReturn>> {\n const { source: sourceOrFactory, toastId, streaming, onItem, delayConfig } = options;\n // Resolve factory function if needed\n const source = resolveStreamSource(sourceOrFactory);\n\n const items: TYield[] = [];\n let count = 0;\n let isFirst = true;\n\n const updateToast = async (item: TYield) => {\n // Delay before first item (after loading)\n if (isFirst && delayConfig.first) {\n await wait(delayConfig.first);\n isFirst = false;\n } else if (!isFirst && delayConfig.items) {\n // Delay between items (not before first)\n await wait(delayConfig.items);\n } else {\n isFirst = false;\n }\n\n count += 1;\n items.push(item);\n onItem?.(item, count);\n\n const streamingData: StreamingData<TYield> = {\n count,\n latest: item,\n items: [...items],\n };\n const { message: streamingMsg, options: streamingOptions } = await resolveResult(\n streaming,\n streamingData,\n `Streaming... (${count} items)`,\n );\n toast.loading(streamingMsg, { id: toastId, ...streamingOptions });\n };\n\n try {\n // Handle sync Generator specially to capture return value\n if (isGenerator<TYield, TReturn>(source)) {\n let result = source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncGenerator specially to capture return value\n if (isAsyncGenerator<TYield, TReturn>(source)) {\n let result = await source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncChannel specially to capture return value\n if (source instanceof AsyncChannel) {\n const iterator = source[Symbol.asyncIterator]();\n let result = await iterator.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await iterator.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // For plain iterables/iterators, convert and iterate (no return value)\n const iterable =\n isAsyncIterable(source) || isIterable(source)\n ? toAsyncIterable(source)\n : toAsyncIterable(source as AsyncIterator<TYield>);\n\n for await (const item of iterable) {\n await updateToast(item);\n }\n\n return { ok: true, items, returnValue: undefined as TReturn };\n } catch (error) {\n return { ok: false, items, error };\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStream overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Like `toast.promise` but for iterables/generators/streams.\n *\n * Handles: loading → streaming → success/error\n *\n * Supports sync and async sources. Captures generator return values.\n * Accepts either an instance or a factory function.\n *\n * @example\n * ```tsx\n * // Pass generator function directly (recommended)\n * await toastStream(fetchData, { ... });\n *\n * // Or pass the result of calling it\n * await toastStream(fetchData(), { ... });\n * ```\n */\nexport function toastStream<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** Sync Generator with return value */\nexport function toastStream<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncChannel with return type - must come before AsyncIterable overload */\nexport function toastStream<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncIterable (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterable<TYield> | (() => AsyncIterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** AsyncIterator (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterable (no return value) */\nexport function toastStream<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterator (no return value) */\nexport function toastStream<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n// Implementation\nexport async function toastStream<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): Promise<StreamResult<TYield, TReturn>> {\n const {\n loading,\n streaming,\n success,\n error,\n finally: onFinally,\n onItem,\n delay,\n ...toastOptions\n } = data;\n\n const delayConfig = normalizeDelay(delay);\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n const toastId = toast.loading(loadingMsg, {\n ...toastOptions,\n ...loadingOptions,\n });\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, { id: toastId, ...successOptions });\n return { items: result.items, returnValue: result.returnValue };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStreamAsync overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Return type matching toast.promise's unwrap pattern */\ntype ToastStreamReturn<TYield, TReturn> = (string | number) & {\n unwrap: () => Promise<StreamResult<TYield, TReturn>>;\n};\n\n/**\n * Non-blocking version that returns immediately like `toast.promise`.\n * Returns toast ID with `unwrap()` method to await the result.\n * Accepts either an instance or a factory function.\n *\n * @example\n * ```ts\n * const { unwrap } = toastStreamAsync(fetchData, { loading: \"...\" });\n * const { items, returnValue } = await unwrap();\n * ```\n */\nexport function toastStreamAsync<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** Sync Generator with return value */\nexport function toastStreamAsync<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncChannel with return type */\nexport function toastStreamAsync<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncIterable - excludes AsyncChannel which has its own overload */\nexport function toastStreamAsync<TYield>(\n source:\n | Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>\n | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** AsyncIterator */\nexport function toastStreamAsync<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterable */\nexport function toastStreamAsync<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterator */\nexport function toastStreamAsync<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n// Implementation\nexport function toastStreamAsync<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): ToastStreamReturn<TYield, TReturn> {\n const { loading, delay, ...rest } = data;\n\n const delayConfig = normalizeDelay(delay);\n // For non-blocking version, show simple loading immediately\n // Callbacks and extended results are resolved inside the promise\n const needsAsyncResolve =\n typeof loading === \"function\" ||\n (typeof loading === \"object\" && loading !== null && \"message\" in loading);\n const initialLoadingMsg = needsAsyncResolve\n ? \"Processing...\"\n : ((loading as ReactNode) ?? \"Processing...\");\n const toastId = toast.loading(initialLoadingMsg);\n\n const promise = (async () => {\n const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;\n\n // If loading needs async resolution (callback or extended result), resolve and update\n if (needsAsyncResolve) {\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n toast.loading(loadingMsg, { id: toastId, ...loadingOptions });\n }\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...toastOptions, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, {\n id: toastId,\n ...toastOptions,\n ...successOptions,\n });\n return { items: result.items, returnValue: result.returnValue };\n })();\n\n // Match toast.promise's return signature\n const res = toastId as ToastStreamReturn<TYield, TReturn>;\n res.unwrap = () => promise;\n return res;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"entry-sonner.cjs","names":["resolveStreamSource","wait","isGenerator","isAsyncGenerator","AsyncChannel","isAsyncIterable","isIterable","toAsyncIterable","toast"],"sources":["../src/toast-stream.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport {\n AsyncChannel,\n isAsyncGenerator,\n isAsyncIterable,\n isGenerator,\n isIterable,\n resolveStreamSource,\n type StreamSource,\n toAsyncIterable,\n wait,\n} from \"./index.ts\";\nimport { type ExternalToast, toast } from \"sonner\";\n\n// Types matching Sonner's patterns\ntype StreamExternalToast = Omit<ExternalToast, \"description\">;\n\ninterface StreamExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\n/** Result can be string, ReactNode, or callback returning either (sync or async) */\ntype StreamResultT<TData = unknown> =\n | string\n | ReactNode\n | ((data: TData) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended result includes full toast options */\ntype StreamExtendedResultT<TData = unknown> =\n | StreamExtendedResult\n | ((data: TData) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Error result - callback receives error first, then partial data */\ntype StreamErrorResultT<TYield> =\n | string\n | ReactNode\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended error result with toast options */\ntype StreamExtendedErrorResultT<TYield> =\n | StreamExtendedResult\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Streaming progress data passed to callbacks */\ntype StreamingData<T> = {\n count: number;\n latest: T;\n /** All items received so far (including latest) */\n items: T[];\n};\n\n/** Success data passed to callbacks - includes return value if generator has one */\ntype SuccessData<TYield, TReturn = void> = {\n count: number;\n items: TYield[];\n /** The return value from the generator (if any) */\n returnValue: TReturn;\n};\n\n/** Partial data available when an error occurs */\ntype ErrorPartialData<TYield> = {\n /** Number of items received before error */\n count: number;\n /** Items received before error */\n items: TYield[];\n};\n\n/** Result of consuming a stream */\ntype StreamResult<TYield, TReturn> = {\n items: TYield[];\n returnValue: TReturn;\n};\n\n/** Internal result from consumeStream - includes partial data on error */\ntype ConsumeResult<TYield, TReturn> =\n | { ok: true; items: TYield[]; returnValue: TReturn }\n | { ok: false; items: TYield[]; error: unknown };\n\n/** Granular delay configuration */\ntype DelayConfig = {\n /** Delay between items in ms */\n items?: number;\n /** Delay after loading, before first item */\n first?: number;\n /** Delay before showing success/error */\n result?: number;\n};\n\n/** Loading result - no data, just static or callback */\ntype LoadingResultT = string | ReactNode | (() => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended loading result with toast options */\ninterface LoadingExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\ntype LoadingExtendedResultT =\n | LoadingExtendedResult\n | (() => LoadingExtendedResult | Promise<LoadingExtendedResult>);\n\n/** Options matching Sonner's PromiseData pattern */\ntype ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {\n /**\n * Message shown while waiting for first item.\n * Supports string, ReactNode, callback, or extended result with toast options.\n */\n loading?: LoadingResultT | LoadingExtendedResultT;\n /** Message shown while streaming - updates on each item */\n streaming?: StreamResultT<StreamingData<TYield>> | StreamExtendedResultT<StreamingData<TYield>>;\n /** Message shown on successful completion */\n success?:\n | StreamResultT<SuccessData<TYield, TReturn>>\n | StreamExtendedResultT<SuccessData<TYield, TReturn>>;\n /** Message shown on error - callback receives (error, { count, items }) */\n error?: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield>;\n /** Called when stream ends (success or error) */\n finally?: () => void | Promise<void>;\n /** Called for each item received */\n onItem?: (item: TYield, count: number) => void;\n /**\n * Artificial delay for visualizing sync streams.\n * - `number`: applies to all phases (first, items, result)\n * - `object`: granular control over individual phases\n *\n * @example\n * // Simple: 500ms for all phases (first, between items, before result)\n * delay: 500\n *\n * @example\n * // Granular: different delays for different phases\n * delay: { first: 500, items: 300, result: 200 }\n */\n delay?: number | DelayConfig;\n};\n\n/** Resolve a result that can be string, ReactNode, or callback */\nasync function resolveResult<TData>(\n result: StreamResultT<TData> | StreamExtendedResultT<TData> | undefined,\n data: TData,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(data);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve an error result that receives (error, partialData) */\nasync function resolveErrorResult<TYield>(\n result: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield> | undefined,\n error: unknown,\n partial: ErrorPartialData<TYield>,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(error, partial);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve loading result - no data passed to callback */\nasync function resolveLoading(\n result: LoadingResultT | LoadingExtendedResultT | undefined,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result();\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as LoadingExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as LoadingExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Normalize delay option to full DelayConfig */\nfunction normalizeDelay(delay: number | DelayConfig | undefined): DelayConfig {\n if (delay === undefined) {\n return {};\n }\n if (typeof delay === \"number\") {\n // Simple number applies to all phases: first, items, and result\n return { first: delay, items: delay, result: delay };\n }\n return delay;\n}\n\n/** Options for consumeStream */\ntype ConsumeStreamOptions<TYield, TReturn> = {\n source: StreamSource<TYield>;\n toastId: string | number;\n streaming: ToastStreamData<TYield, TReturn>[\"streaming\"];\n onItem: ToastStreamData<TYield, TReturn>[\"onItem\"];\n delayConfig: DelayConfig;\n};\n\n/** Core streaming logic - consumes source and updates toast, captures return value */\nasync function consumeStream<TYield, TReturn>(\n options: ConsumeStreamOptions<TYield, TReturn>,\n): Promise<ConsumeResult<TYield, TReturn>> {\n const { source: sourceOrFactory, toastId, streaming, onItem, delayConfig } = options;\n // Resolve factory function if needed\n const source = resolveStreamSource(sourceOrFactory);\n\n const items: TYield[] = [];\n let count = 0;\n let isFirst = true;\n\n const updateToast = async (item: TYield) => {\n // Delay before first item (after loading)\n if (isFirst && delayConfig.first) {\n await wait(delayConfig.first);\n isFirst = false;\n } else if (!isFirst && delayConfig.items) {\n // Delay between items (not before first)\n await wait(delayConfig.items);\n } else {\n isFirst = false;\n }\n\n count += 1;\n items.push(item);\n onItem?.(item, count);\n\n const streamingData: StreamingData<TYield> = {\n count,\n latest: item,\n items: [...items],\n };\n const { message: streamingMsg, options: streamingOptions } = await resolveResult(\n streaming,\n streamingData,\n `Streaming... (${count} items)`,\n );\n toast.loading(streamingMsg, { id: toastId, ...streamingOptions });\n };\n\n try {\n // Handle sync Generator specially to capture return value\n if (isGenerator<TYield, TReturn>(source)) {\n let result = source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncGenerator specially to capture return value\n if (isAsyncGenerator<TYield, TReturn>(source)) {\n let result = await source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncChannel specially to capture return value\n if (source instanceof AsyncChannel) {\n const iterator = source[Symbol.asyncIterator]();\n let result = await iterator.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await iterator.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // For plain iterables/iterators, convert and iterate (no return value)\n const iterable =\n isAsyncIterable(source) || isIterable(source)\n ? toAsyncIterable(source)\n : toAsyncIterable(source as AsyncIterator<TYield>);\n\n for await (const item of iterable) {\n await updateToast(item);\n }\n\n return { ok: true, items, returnValue: undefined as TReturn };\n } catch (error) {\n return { ok: false, items, error };\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStream overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Like `toast.promise` but for iterables/generators/streams.\n *\n * Blocking/awaitable API: waits until the stream completes and resolves to\n * `{ items, returnValue }`. Use this when the caller should wait for all\n * streamed values before continuing.\n *\n * Handles: loading → streaming → success/error.\n *\n * Supports sync and async sources. Captures generator return values.\n * Accepts either an instance or a factory function.\n *\n * @example Async generator with progress and return value\n * ```ts\n * import { toastStream } from \"xantiagoma/sonner\";\n *\n * async function* importUsers() {\n * yield { id: 1, name: \"Ada\" };\n * yield { id: 2, name: \"Grace\" };\n * return { imported: 2 };\n * }\n *\n * const { items, returnValue } = await toastStream(importUsers, {\n * loading: \"Importing users...\",\n * streaming: ({ count, latest }) => `Imported ${count}: ${latest.name}`,\n * success: ({ count, returnValue }) =>\n * `Imported ${count} users (${returnValue.imported} confirmed)`,\n * error: (error, partial) =>\n * `Import failed after ${partial.count} users: ${String(error)}`,\n * });\n * ```\n *\n * @example Sync iterable with artificial delay\n * ```ts\n * const result = await toastStream([\"parse\", \"validate\", \"save\"], {\n * loading: \"Starting...\",\n * streaming: ({ latest }) => `Running ${latest}`,\n * success: ({ count }) => `Completed ${count} steps`,\n * delay: { first: 300, items: 200, result: 150 },\n * });\n * ```\n */\nexport function toastStream<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** Sync Generator with return value */\nexport function toastStream<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncChannel with return type - must come before AsyncIterable overload */\nexport function toastStream<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncIterable (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterable<TYield> | (() => AsyncIterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** AsyncIterator (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterable (no return value) */\nexport function toastStream<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterator (no return value) */\nexport function toastStream<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n// Implementation\nexport async function toastStream<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): Promise<StreamResult<TYield, TReturn>> {\n const {\n loading,\n streaming,\n success,\n error,\n finally: onFinally,\n onItem,\n delay,\n ...toastOptions\n } = data;\n\n const delayConfig = normalizeDelay(delay);\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n const toastId = toast.loading(loadingMsg, {\n ...toastOptions,\n ...loadingOptions,\n });\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, { id: toastId, ...successOptions });\n return { items: result.items, returnValue: result.returnValue };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStreamAsync overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Return type matching toast.promise's unwrap pattern */\ntype ToastStreamReturn<TYield, TReturn> = (string | number) & {\n unwrap: () => Promise<StreamResult<TYield, TReturn>>;\n};\n\n/**\n * Non-blocking version that returns immediately like `toast.promise`.\n * Returns the toast ID with an `unwrap()` method to await the result later.\n * Accepts either an instance or a factory function.\n *\n * Use this when the UI should continue immediately while the toast tracks the\n * stream in the background.\n *\n * @example\n * ```ts\n * import { toastStreamAsync } from \"xantiagoma/sonner\";\n *\n * const toastId = toastStreamAsync(importUsers, {\n * loading: \"Importing users...\",\n * streaming: ({ count }) => `Imported ${count}`,\n * success: ({ count }) => `Imported ${count} users`,\n * });\n *\n * // The caller continues immediately; await only if/when the result matters.\n * const { items, returnValue } = await toastId.unwrap();\n * ```\n *\n * @example Fire-and-track with error handling\n * ```ts\n * const toastId = toastStreamAsync(uploadFiles(files), {\n * loading: \"Uploading...\",\n * streaming: ({ count }) => `${count} files uploaded`,\n * error: (error, partial) =>\n * `Upload failed after ${partial.count} files: ${String(error)}`,\n * });\n *\n * toastId.unwrap().catch((error) => {\n * reportError(error);\n * });\n * ```\n */\nexport function toastStreamAsync<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** Sync Generator with return value */\nexport function toastStreamAsync<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncChannel with return type */\nexport function toastStreamAsync<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncIterable - excludes AsyncChannel which has its own overload */\nexport function toastStreamAsync<TYield>(\n source:\n | Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>\n | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** AsyncIterator */\nexport function toastStreamAsync<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterable */\nexport function toastStreamAsync<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterator */\nexport function toastStreamAsync<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n// Implementation\nexport function toastStreamAsync<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): ToastStreamReturn<TYield, TReturn> {\n const { loading, delay, ...rest } = data;\n\n const delayConfig = normalizeDelay(delay);\n // For non-blocking version, show simple loading immediately\n // Callbacks and extended results are resolved inside the promise\n const needsAsyncResolve =\n typeof loading === \"function\" ||\n (typeof loading === \"object\" && loading !== null && \"message\" in loading);\n const initialLoadingMsg = needsAsyncResolve\n ? \"Processing...\"\n : ((loading as ReactNode) ?? \"Processing...\");\n const toastId = toast.loading(initialLoadingMsg);\n\n const promise = (async () => {\n const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;\n\n // If loading needs async resolution (callback or extended result), resolve and update\n if (needsAsyncResolve) {\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n toast.loading(loadingMsg, { id: toastId, ...loadingOptions });\n }\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...toastOptions, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, {\n id: toastId,\n ...toastOptions,\n ...successOptions,\n });\n return { items: result.items, returnValue: result.returnValue };\n })();\n\n // Match toast.promise's return signature\n const res = toastId as ToastStreamReturn<TYield, TReturn>;\n res.unwrap = () => promise;\n return res;\n}\n"],"mappings":"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"}
@@ -80,18 +80,43 @@ type ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {
80
80
  /**
81
81
  * Like `toast.promise` but for iterables/generators/streams.
82
82
  *
83
- * Handles: loading streaming success/error
83
+ * Blocking/awaitable API: waits until the stream completes and resolves to
84
+ * `{ items, returnValue }`. Use this when the caller should wait for all
85
+ * streamed values before continuing.
86
+ *
87
+ * Handles: loading → streaming → success/error.
84
88
  *
85
89
  * Supports sync and async sources. Captures generator return values.
86
90
  * Accepts either an instance or a factory function.
87
91
  *
88
- * @example
89
- * ```tsx
90
- * // Pass generator function directly (recommended)
91
- * await toastStream(fetchData, { ... });
92
+ * @example Async generator with progress and return value
93
+ * ```ts
94
+ * import { toastStream } from "xantiagoma/sonner";
95
+ *
96
+ * async function* importUsers() {
97
+ * yield { id: 1, name: "Ada" };
98
+ * yield { id: 2, name: "Grace" };
99
+ * return { imported: 2 };
100
+ * }
101
+ *
102
+ * const { items, returnValue } = await toastStream(importUsers, {
103
+ * loading: "Importing users...",
104
+ * streaming: ({ count, latest }) => `Imported ${count}: ${latest.name}`,
105
+ * success: ({ count, returnValue }) =>
106
+ * `Imported ${count} users (${returnValue.imported} confirmed)`,
107
+ * error: (error, partial) =>
108
+ * `Import failed after ${partial.count} users: ${String(error)}`,
109
+ * });
110
+ * ```
92
111
  *
93
- * // Or pass the result of calling it
94
- * await toastStream(fetchData(), { ... });
112
+ * @example Sync iterable with artificial delay
113
+ * ```ts
114
+ * const result = await toastStream(["parse", "validate", "save"], {
115
+ * loading: "Starting...",
116
+ * streaming: ({ latest }) => `Running ${latest}`,
117
+ * success: ({ count }) => `Completed ${count} steps`,
118
+ * delay: { first: 300, items: 200, result: 150 },
119
+ * });
95
120
  * ```
96
121
  */
97
122
  declare function toastStream<TYield, TReturn>(source: AsyncGenerator<TYield, TReturn, unknown> | (() => AsyncGenerator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): Promise<StreamResult<TYield, TReturn>>;
@@ -107,6 +132,59 @@ declare function toastStream<TYield>(source: AsyncIterator<TYield> | (() => Asyn
107
132
  declare function toastStream<TYield>(source: Iterable<TYield> | (() => Iterable<TYield>), data?: ToastStreamData<TYield, void>): Promise<StreamResult<TYield, void>>;
108
133
  /** Sync Iterator (no return value) */
109
134
  declare function toastStream<TYield>(source: Iterator<TYield> | (() => Iterator<TYield>), data?: ToastStreamData<TYield, void>): Promise<StreamResult<TYield, void>>;
135
+ /** Return type matching toast.promise's unwrap pattern */
136
+ type ToastStreamReturn<TYield, TReturn> = (string | number) & {
137
+ unwrap: () => Promise<StreamResult<TYield, TReturn>>;
138
+ };
139
+ /**
140
+ * Non-blocking version that returns immediately like `toast.promise`.
141
+ * Returns the toast ID with an `unwrap()` method to await the result later.
142
+ * Accepts either an instance or a factory function.
143
+ *
144
+ * Use this when the UI should continue immediately while the toast tracks the
145
+ * stream in the background.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import { toastStreamAsync } from "xantiagoma/sonner";
150
+ *
151
+ * const toastId = toastStreamAsync(importUsers, {
152
+ * loading: "Importing users...",
153
+ * streaming: ({ count }) => `Imported ${count}`,
154
+ * success: ({ count }) => `Imported ${count} users`,
155
+ * });
156
+ *
157
+ * // The caller continues immediately; await only if/when the result matters.
158
+ * const { items, returnValue } = await toastId.unwrap();
159
+ * ```
160
+ *
161
+ * @example Fire-and-track with error handling
162
+ * ```ts
163
+ * const toastId = toastStreamAsync(uploadFiles(files), {
164
+ * loading: "Uploading...",
165
+ * streaming: ({ count }) => `${count} files uploaded`,
166
+ * error: (error, partial) =>
167
+ * `Upload failed after ${partial.count} files: ${String(error)}`,
168
+ * });
169
+ *
170
+ * toastId.unwrap().catch((error) => {
171
+ * reportError(error);
172
+ * });
173
+ * ```
174
+ */
175
+ declare function toastStreamAsync<TYield, TReturn>(source: AsyncGenerator<TYield, TReturn, unknown> | (() => AsyncGenerator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
176
+ /** Sync Generator with return value */
177
+ declare function toastStreamAsync<TYield, TReturn>(source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
178
+ /** AsyncChannel with return type */
179
+ declare function toastStreamAsync<TYield, TReturn>(source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
180
+ /** AsyncIterable - excludes AsyncChannel which has its own overload */
181
+ declare function toastStreamAsync<TYield>(source: Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>> | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
182
+ /** AsyncIterator */
183
+ declare function toastStreamAsync<TYield>(source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
184
+ /** Sync Iterable */
185
+ declare function toastStreamAsync<TYield>(source: Iterable<TYield> | (() => Iterable<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
186
+ /** Sync Iterator */
187
+ declare function toastStreamAsync<TYield>(source: Iterator<TYield> | (() => Iterator<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
110
188
  //#endregion
111
- export { toastStream };
189
+ export { toastStream, toastStreamAsync };
112
190
  //# sourceMappingURL=entry-sonner.d.cts.map
@@ -80,18 +80,43 @@ type ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {
80
80
  /**
81
81
  * Like `toast.promise` but for iterables/generators/streams.
82
82
  *
83
- * Handles: loading streaming success/error
83
+ * Blocking/awaitable API: waits until the stream completes and resolves to
84
+ * `{ items, returnValue }`. Use this when the caller should wait for all
85
+ * streamed values before continuing.
86
+ *
87
+ * Handles: loading → streaming → success/error.
84
88
  *
85
89
  * Supports sync and async sources. Captures generator return values.
86
90
  * Accepts either an instance or a factory function.
87
91
  *
88
- * @example
89
- * ```tsx
90
- * // Pass generator function directly (recommended)
91
- * await toastStream(fetchData, { ... });
92
+ * @example Async generator with progress and return value
93
+ * ```ts
94
+ * import { toastStream } from "xantiagoma/sonner";
95
+ *
96
+ * async function* importUsers() {
97
+ * yield { id: 1, name: "Ada" };
98
+ * yield { id: 2, name: "Grace" };
99
+ * return { imported: 2 };
100
+ * }
101
+ *
102
+ * const { items, returnValue } = await toastStream(importUsers, {
103
+ * loading: "Importing users...",
104
+ * streaming: ({ count, latest }) => `Imported ${count}: ${latest.name}`,
105
+ * success: ({ count, returnValue }) =>
106
+ * `Imported ${count} users (${returnValue.imported} confirmed)`,
107
+ * error: (error, partial) =>
108
+ * `Import failed after ${partial.count} users: ${String(error)}`,
109
+ * });
110
+ * ```
92
111
  *
93
- * // Or pass the result of calling it
94
- * await toastStream(fetchData(), { ... });
112
+ * @example Sync iterable with artificial delay
113
+ * ```ts
114
+ * const result = await toastStream(["parse", "validate", "save"], {
115
+ * loading: "Starting...",
116
+ * streaming: ({ latest }) => `Running ${latest}`,
117
+ * success: ({ count }) => `Completed ${count} steps`,
118
+ * delay: { first: 300, items: 200, result: 150 },
119
+ * });
95
120
  * ```
96
121
  */
97
122
  declare function toastStream<TYield, TReturn>(source: AsyncGenerator<TYield, TReturn, unknown> | (() => AsyncGenerator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): Promise<StreamResult<TYield, TReturn>>;
@@ -107,6 +132,59 @@ declare function toastStream<TYield>(source: AsyncIterator<TYield> | (() => Asyn
107
132
  declare function toastStream<TYield>(source: Iterable<TYield> | (() => Iterable<TYield>), data?: ToastStreamData<TYield, void>): Promise<StreamResult<TYield, void>>;
108
133
  /** Sync Iterator (no return value) */
109
134
  declare function toastStream<TYield>(source: Iterator<TYield> | (() => Iterator<TYield>), data?: ToastStreamData<TYield, void>): Promise<StreamResult<TYield, void>>;
135
+ /** Return type matching toast.promise's unwrap pattern */
136
+ type ToastStreamReturn<TYield, TReturn> = (string | number) & {
137
+ unwrap: () => Promise<StreamResult<TYield, TReturn>>;
138
+ };
139
+ /**
140
+ * Non-blocking version that returns immediately like `toast.promise`.
141
+ * Returns the toast ID with an `unwrap()` method to await the result later.
142
+ * Accepts either an instance or a factory function.
143
+ *
144
+ * Use this when the UI should continue immediately while the toast tracks the
145
+ * stream in the background.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import { toastStreamAsync } from "xantiagoma/sonner";
150
+ *
151
+ * const toastId = toastStreamAsync(importUsers, {
152
+ * loading: "Importing users...",
153
+ * streaming: ({ count }) => `Imported ${count}`,
154
+ * success: ({ count }) => `Imported ${count} users`,
155
+ * });
156
+ *
157
+ * // The caller continues immediately; await only if/when the result matters.
158
+ * const { items, returnValue } = await toastId.unwrap();
159
+ * ```
160
+ *
161
+ * @example Fire-and-track with error handling
162
+ * ```ts
163
+ * const toastId = toastStreamAsync(uploadFiles(files), {
164
+ * loading: "Uploading...",
165
+ * streaming: ({ count }) => `${count} files uploaded`,
166
+ * error: (error, partial) =>
167
+ * `Upload failed after ${partial.count} files: ${String(error)}`,
168
+ * });
169
+ *
170
+ * toastId.unwrap().catch((error) => {
171
+ * reportError(error);
172
+ * });
173
+ * ```
174
+ */
175
+ declare function toastStreamAsync<TYield, TReturn>(source: AsyncGenerator<TYield, TReturn, unknown> | (() => AsyncGenerator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
176
+ /** Sync Generator with return value */
177
+ declare function toastStreamAsync<TYield, TReturn>(source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
178
+ /** AsyncChannel with return type */
179
+ declare function toastStreamAsync<TYield, TReturn>(source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>), data?: ToastStreamData<TYield, TReturn>): ToastStreamReturn<TYield, TReturn>;
180
+ /** AsyncIterable - excludes AsyncChannel which has its own overload */
181
+ declare function toastStreamAsync<TYield>(source: Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>> | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
182
+ /** AsyncIterator */
183
+ declare function toastStreamAsync<TYield>(source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
184
+ /** Sync Iterable */
185
+ declare function toastStreamAsync<TYield>(source: Iterable<TYield> | (() => Iterable<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
186
+ /** Sync Iterator */
187
+ declare function toastStreamAsync<TYield>(source: Iterator<TYield> | (() => Iterator<TYield>), data?: ToastStreamData<TYield, void>): ToastStreamReturn<TYield, void>;
110
188
  //#endregion
111
- export { toastStream };
189
+ export { toastStream, toastStreamAsync };
112
190
  //# sourceMappingURL=entry-sonner.d.mts.map
@@ -202,7 +202,63 @@ async function toastStream(source, data = {}) {
202
202
  returnValue: result.returnValue
203
203
  };
204
204
  }
205
+ function toastStreamAsync(source, data = {}) {
206
+ const { loading, delay, ...rest } = data;
207
+ const delayConfig = normalizeDelay(delay);
208
+ const needsAsyncResolve = typeof loading === "function" || typeof loading === "object" && loading !== null && "message" in loading;
209
+ const initialLoadingMsg = needsAsyncResolve ? "Processing..." : loading ?? "Processing...";
210
+ const toastId = toast.loading(initialLoadingMsg);
211
+ const promise = (async () => {
212
+ const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;
213
+ if (needsAsyncResolve) {
214
+ const { message: loadingMsg, options: loadingOptions } = await resolveLoading(loading, "Processing...");
215
+ toast.loading(loadingMsg, {
216
+ id: toastId,
217
+ ...loadingOptions
218
+ });
219
+ }
220
+ const result = await consumeStream({
221
+ source,
222
+ toastId,
223
+ streaming,
224
+ onItem,
225
+ delayConfig
226
+ });
227
+ await onFinally?.();
228
+ if (delayConfig.result) await wait(delayConfig.result);
229
+ if (!result.ok) {
230
+ const partial = {
231
+ count: result.items.length,
232
+ items: result.items
233
+ };
234
+ const { message: errorMsg, options: errorOptions } = await resolveErrorResult(error, result.error, partial, "Stream failed");
235
+ toast.error(errorMsg, {
236
+ id: toastId,
237
+ ...toastOptions,
238
+ ...errorOptions
239
+ });
240
+ throw result.error;
241
+ }
242
+ const { message: successMsg, options: successOptions } = await resolveResult(success, {
243
+ count: result.items.length,
244
+ items: result.items,
245
+ returnValue: result.returnValue
246
+ }, `Complete! ${result.items.length} items`);
247
+ toast.success(successMsg, {
248
+ id: toastId,
249
+ ...toastOptions,
250
+ ...successOptions
251
+ });
252
+ return {
253
+ items: result.items,
254
+ returnValue: result.returnValue
255
+ };
256
+ })();
257
+ const res = toastId;
258
+ res.unwrap = () => promise;
259
+ return res;
260
+ }
205
261
  //#endregion
206
- export { toastStream };
262
+ export { toastStream, toastStreamAsync };
207
263
 
208
264
  //# sourceMappingURL=entry-sonner.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"entry-sonner.mjs","names":[],"sources":["../src/toast-stream.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport {\n AsyncChannel,\n isAsyncGenerator,\n isAsyncIterable,\n isGenerator,\n isIterable,\n resolveStreamSource,\n type StreamSource,\n toAsyncIterable,\n wait,\n} from \"./index.ts\";\nimport { type ExternalToast, toast } from \"sonner\";\n\n// Types matching Sonner's patterns\ntype StreamExternalToast = Omit<ExternalToast, \"description\">;\n\ninterface StreamExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\n/** Result can be string, ReactNode, or callback returning either (sync or async) */\ntype StreamResultT<TData = unknown> =\n | string\n | ReactNode\n | ((data: TData) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended result includes full toast options */\ntype StreamExtendedResultT<TData = unknown> =\n | StreamExtendedResult\n | ((data: TData) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Error result - callback receives error first, then partial data */\ntype StreamErrorResultT<TYield> =\n | string\n | ReactNode\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended error result with toast options */\ntype StreamExtendedErrorResultT<TYield> =\n | StreamExtendedResult\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Streaming progress data passed to callbacks */\ntype StreamingData<T> = {\n count: number;\n latest: T;\n /** All items received so far (including latest) */\n items: T[];\n};\n\n/** Success data passed to callbacks - includes return value if generator has one */\ntype SuccessData<TYield, TReturn = void> = {\n count: number;\n items: TYield[];\n /** The return value from the generator (if any) */\n returnValue: TReturn;\n};\n\n/** Partial data available when an error occurs */\ntype ErrorPartialData<TYield> = {\n /** Number of items received before error */\n count: number;\n /** Items received before error */\n items: TYield[];\n};\n\n/** Result of consuming a stream */\ntype StreamResult<TYield, TReturn> = {\n items: TYield[];\n returnValue: TReturn;\n};\n\n/** Internal result from consumeStream - includes partial data on error */\ntype ConsumeResult<TYield, TReturn> =\n | { ok: true; items: TYield[]; returnValue: TReturn }\n | { ok: false; items: TYield[]; error: unknown };\n\n/** Granular delay configuration */\ntype DelayConfig = {\n /** Delay between items in ms */\n items?: number;\n /** Delay after loading, before first item */\n first?: number;\n /** Delay before showing success/error */\n result?: number;\n};\n\n/** Loading result - no data, just static or callback */\ntype LoadingResultT = string | ReactNode | (() => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended loading result with toast options */\ninterface LoadingExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\ntype LoadingExtendedResultT =\n | LoadingExtendedResult\n | (() => LoadingExtendedResult | Promise<LoadingExtendedResult>);\n\n/** Options matching Sonner's PromiseData pattern */\ntype ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {\n /**\n * Message shown while waiting for first item.\n * Supports string, ReactNode, callback, or extended result with toast options.\n */\n loading?: LoadingResultT | LoadingExtendedResultT;\n /** Message shown while streaming - updates on each item */\n streaming?: StreamResultT<StreamingData<TYield>> | StreamExtendedResultT<StreamingData<TYield>>;\n /** Message shown on successful completion */\n success?:\n | StreamResultT<SuccessData<TYield, TReturn>>\n | StreamExtendedResultT<SuccessData<TYield, TReturn>>;\n /** Message shown on error - callback receives (error, { count, items }) */\n error?: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield>;\n /** Called when stream ends (success or error) */\n finally?: () => void | Promise<void>;\n /** Called for each item received */\n onItem?: (item: TYield, count: number) => void;\n /**\n * Artificial delay for visualizing sync streams.\n * - `number`: applies to all phases (first, items, result)\n * - `object`: granular control over individual phases\n *\n * @example\n * // Simple: 500ms for all phases (first, between items, before result)\n * delay: 500\n *\n * @example\n * // Granular: different delays for different phases\n * delay: { first: 500, items: 300, result: 200 }\n */\n delay?: number | DelayConfig;\n};\n\n/** Resolve a result that can be string, ReactNode, or callback */\nasync function resolveResult<TData>(\n result: StreamResultT<TData> | StreamExtendedResultT<TData> | undefined,\n data: TData,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(data);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve an error result that receives (error, partialData) */\nasync function resolveErrorResult<TYield>(\n result: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield> | undefined,\n error: unknown,\n partial: ErrorPartialData<TYield>,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(error, partial);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve loading result - no data passed to callback */\nasync function resolveLoading(\n result: LoadingResultT | LoadingExtendedResultT | undefined,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result();\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as LoadingExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as LoadingExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Normalize delay option to full DelayConfig */\nfunction normalizeDelay(delay: number | DelayConfig | undefined): DelayConfig {\n if (delay === undefined) {\n return {};\n }\n if (typeof delay === \"number\") {\n // Simple number applies to all phases: first, items, and result\n return { first: delay, items: delay, result: delay };\n }\n return delay;\n}\n\n/** Options for consumeStream */\ntype ConsumeStreamOptions<TYield, TReturn> = {\n source: StreamSource<TYield>;\n toastId: string | number;\n streaming: ToastStreamData<TYield, TReturn>[\"streaming\"];\n onItem: ToastStreamData<TYield, TReturn>[\"onItem\"];\n delayConfig: DelayConfig;\n};\n\n/** Core streaming logic - consumes source and updates toast, captures return value */\nasync function consumeStream<TYield, TReturn>(\n options: ConsumeStreamOptions<TYield, TReturn>,\n): Promise<ConsumeResult<TYield, TReturn>> {\n const { source: sourceOrFactory, toastId, streaming, onItem, delayConfig } = options;\n // Resolve factory function if needed\n const source = resolveStreamSource(sourceOrFactory);\n\n const items: TYield[] = [];\n let count = 0;\n let isFirst = true;\n\n const updateToast = async (item: TYield) => {\n // Delay before first item (after loading)\n if (isFirst && delayConfig.first) {\n await wait(delayConfig.first);\n isFirst = false;\n } else if (!isFirst && delayConfig.items) {\n // Delay between items (not before first)\n await wait(delayConfig.items);\n } else {\n isFirst = false;\n }\n\n count += 1;\n items.push(item);\n onItem?.(item, count);\n\n const streamingData: StreamingData<TYield> = {\n count,\n latest: item,\n items: [...items],\n };\n const { message: streamingMsg, options: streamingOptions } = await resolveResult(\n streaming,\n streamingData,\n `Streaming... (${count} items)`,\n );\n toast.loading(streamingMsg, { id: toastId, ...streamingOptions });\n };\n\n try {\n // Handle sync Generator specially to capture return value\n if (isGenerator<TYield, TReturn>(source)) {\n let result = source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncGenerator specially to capture return value\n if (isAsyncGenerator<TYield, TReturn>(source)) {\n let result = await source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncChannel specially to capture return value\n if (source instanceof AsyncChannel) {\n const iterator = source[Symbol.asyncIterator]();\n let result = await iterator.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await iterator.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // For plain iterables/iterators, convert and iterate (no return value)\n const iterable =\n isAsyncIterable(source) || isIterable(source)\n ? toAsyncIterable(source)\n : toAsyncIterable(source as AsyncIterator<TYield>);\n\n for await (const item of iterable) {\n await updateToast(item);\n }\n\n return { ok: true, items, returnValue: undefined as TReturn };\n } catch (error) {\n return { ok: false, items, error };\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStream overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Like `toast.promise` but for iterables/generators/streams.\n *\n * Handles: loading → streaming → success/error\n *\n * Supports sync and async sources. Captures generator return values.\n * Accepts either an instance or a factory function.\n *\n * @example\n * ```tsx\n * // Pass generator function directly (recommended)\n * await toastStream(fetchData, { ... });\n *\n * // Or pass the result of calling it\n * await toastStream(fetchData(), { ... });\n * ```\n */\nexport function toastStream<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** Sync Generator with return value */\nexport function toastStream<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncChannel with return type - must come before AsyncIterable overload */\nexport function toastStream<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncIterable (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterable<TYield> | (() => AsyncIterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** AsyncIterator (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterable (no return value) */\nexport function toastStream<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterator (no return value) */\nexport function toastStream<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n// Implementation\nexport async function toastStream<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): Promise<StreamResult<TYield, TReturn>> {\n const {\n loading,\n streaming,\n success,\n error,\n finally: onFinally,\n onItem,\n delay,\n ...toastOptions\n } = data;\n\n const delayConfig = normalizeDelay(delay);\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n const toastId = toast.loading(loadingMsg, {\n ...toastOptions,\n ...loadingOptions,\n });\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, { id: toastId, ...successOptions });\n return { items: result.items, returnValue: result.returnValue };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStreamAsync overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Return type matching toast.promise's unwrap pattern */\ntype ToastStreamReturn<TYield, TReturn> = (string | number) & {\n unwrap: () => Promise<StreamResult<TYield, TReturn>>;\n};\n\n/**\n * Non-blocking version that returns immediately like `toast.promise`.\n * Returns toast ID with `unwrap()` method to await the result.\n * Accepts either an instance or a factory function.\n *\n * @example\n * ```ts\n * const { unwrap } = toastStreamAsync(fetchData, { loading: \"...\" });\n * const { items, returnValue } = await unwrap();\n * ```\n */\nexport function toastStreamAsync<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** Sync Generator with return value */\nexport function toastStreamAsync<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncChannel with return type */\nexport function toastStreamAsync<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncIterable - excludes AsyncChannel which has its own overload */\nexport function toastStreamAsync<TYield>(\n source:\n | Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>\n | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** AsyncIterator */\nexport function toastStreamAsync<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterable */\nexport function toastStreamAsync<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterator */\nexport function toastStreamAsync<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n// Implementation\nexport function toastStreamAsync<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): ToastStreamReturn<TYield, TReturn> {\n const { loading, delay, ...rest } = data;\n\n const delayConfig = normalizeDelay(delay);\n // For non-blocking version, show simple loading immediately\n // Callbacks and extended results are resolved inside the promise\n const needsAsyncResolve =\n typeof loading === \"function\" ||\n (typeof loading === \"object\" && loading !== null && \"message\" in loading);\n const initialLoadingMsg = needsAsyncResolve\n ? \"Processing...\"\n : ((loading as ReactNode) ?? \"Processing...\");\n const toastId = toast.loading(initialLoadingMsg);\n\n const promise = (async () => {\n const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;\n\n // If loading needs async resolution (callback or extended result), resolve and update\n if (needsAsyncResolve) {\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n toast.loading(loadingMsg, { id: toastId, ...loadingOptions });\n }\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...toastOptions, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, {\n id: toastId,\n ...toastOptions,\n ...successOptions,\n });\n return { items: result.items, returnValue: result.returnValue };\n })();\n\n // Match toast.promise's return signature\n const res = toastId as ToastStreamReturn<TYield, TReturn>;\n res.unwrap = () => promise;\n return res;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"entry-sonner.mjs","names":[],"sources":["../src/toast-stream.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport {\n AsyncChannel,\n isAsyncGenerator,\n isAsyncIterable,\n isGenerator,\n isIterable,\n resolveStreamSource,\n type StreamSource,\n toAsyncIterable,\n wait,\n} from \"./index.ts\";\nimport { type ExternalToast, toast } from \"sonner\";\n\n// Types matching Sonner's patterns\ntype StreamExternalToast = Omit<ExternalToast, \"description\">;\n\ninterface StreamExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\n/** Result can be string, ReactNode, or callback returning either (sync or async) */\ntype StreamResultT<TData = unknown> =\n | string\n | ReactNode\n | ((data: TData) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended result includes full toast options */\ntype StreamExtendedResultT<TData = unknown> =\n | StreamExtendedResult\n | ((data: TData) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Error result - callback receives error first, then partial data */\ntype StreamErrorResultT<TYield> =\n | string\n | ReactNode\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended error result with toast options */\ntype StreamExtendedErrorResultT<TYield> =\n | StreamExtendedResult\n | ((\n error: unknown,\n partial: ErrorPartialData<TYield>,\n ) => StreamExtendedResult | Promise<StreamExtendedResult>);\n\n/** Streaming progress data passed to callbacks */\ntype StreamingData<T> = {\n count: number;\n latest: T;\n /** All items received so far (including latest) */\n items: T[];\n};\n\n/** Success data passed to callbacks - includes return value if generator has one */\ntype SuccessData<TYield, TReturn = void> = {\n count: number;\n items: TYield[];\n /** The return value from the generator (if any) */\n returnValue: TReturn;\n};\n\n/** Partial data available when an error occurs */\ntype ErrorPartialData<TYield> = {\n /** Number of items received before error */\n count: number;\n /** Items received before error */\n items: TYield[];\n};\n\n/** Result of consuming a stream */\ntype StreamResult<TYield, TReturn> = {\n items: TYield[];\n returnValue: TReturn;\n};\n\n/** Internal result from consumeStream - includes partial data on error */\ntype ConsumeResult<TYield, TReturn> =\n | { ok: true; items: TYield[]; returnValue: TReturn }\n | { ok: false; items: TYield[]; error: unknown };\n\n/** Granular delay configuration */\ntype DelayConfig = {\n /** Delay between items in ms */\n items?: number;\n /** Delay after loading, before first item */\n first?: number;\n /** Delay before showing success/error */\n result?: number;\n};\n\n/** Loading result - no data, just static or callback */\ntype LoadingResultT = string | ReactNode | (() => ReactNode | string | Promise<ReactNode | string>);\n\n/** Extended loading result with toast options */\ninterface LoadingExtendedResult extends ExternalToast {\n message: ReactNode;\n}\n\ntype LoadingExtendedResultT =\n | LoadingExtendedResult\n | (() => LoadingExtendedResult | Promise<LoadingExtendedResult>);\n\n/** Options matching Sonner's PromiseData pattern */\ntype ToastStreamData<TYield = unknown, TReturn = void> = StreamExternalToast & {\n /**\n * Message shown while waiting for first item.\n * Supports string, ReactNode, callback, or extended result with toast options.\n */\n loading?: LoadingResultT | LoadingExtendedResultT;\n /** Message shown while streaming - updates on each item */\n streaming?: StreamResultT<StreamingData<TYield>> | StreamExtendedResultT<StreamingData<TYield>>;\n /** Message shown on successful completion */\n success?:\n | StreamResultT<SuccessData<TYield, TReturn>>\n | StreamExtendedResultT<SuccessData<TYield, TReturn>>;\n /** Message shown on error - callback receives (error, { count, items }) */\n error?: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield>;\n /** Called when stream ends (success or error) */\n finally?: () => void | Promise<void>;\n /** Called for each item received */\n onItem?: (item: TYield, count: number) => void;\n /**\n * Artificial delay for visualizing sync streams.\n * - `number`: applies to all phases (first, items, result)\n * - `object`: granular control over individual phases\n *\n * @example\n * // Simple: 500ms for all phases (first, between items, before result)\n * delay: 500\n *\n * @example\n * // Granular: different delays for different phases\n * delay: { first: 500, items: 300, result: 200 }\n */\n delay?: number | DelayConfig;\n};\n\n/** Resolve a result that can be string, ReactNode, or callback */\nasync function resolveResult<TData>(\n result: StreamResultT<TData> | StreamExtendedResultT<TData> | undefined,\n data: TData,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(data);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve an error result that receives (error, partialData) */\nasync function resolveErrorResult<TYield>(\n result: StreamErrorResultT<TYield> | StreamExtendedErrorResultT<TYield> | undefined,\n error: unknown,\n partial: ErrorPartialData<TYield>,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result(error, partial);\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as StreamExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as StreamExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Resolve loading result - no data passed to callback */\nasync function resolveLoading(\n result: LoadingResultT | LoadingExtendedResultT | undefined,\n fallback: string,\n): Promise<{ message: ReactNode; options?: ExternalToast }> {\n if (result === undefined) {\n return { message: fallback };\n }\n\n if (typeof result === \"function\") {\n const resolved = await result();\n if (typeof resolved === \"object\" && resolved !== null && \"message\" in resolved) {\n const { message, ...options } = resolved as LoadingExtendedResult;\n return { message, options };\n }\n return { message: resolved as ReactNode };\n }\n\n if (typeof result === \"object\" && result !== null && \"message\" in result) {\n const { message, ...options } = result as LoadingExtendedResult;\n return { message, options };\n }\n\n return { message: result };\n}\n\n/** Normalize delay option to full DelayConfig */\nfunction normalizeDelay(delay: number | DelayConfig | undefined): DelayConfig {\n if (delay === undefined) {\n return {};\n }\n if (typeof delay === \"number\") {\n // Simple number applies to all phases: first, items, and result\n return { first: delay, items: delay, result: delay };\n }\n return delay;\n}\n\n/** Options for consumeStream */\ntype ConsumeStreamOptions<TYield, TReturn> = {\n source: StreamSource<TYield>;\n toastId: string | number;\n streaming: ToastStreamData<TYield, TReturn>[\"streaming\"];\n onItem: ToastStreamData<TYield, TReturn>[\"onItem\"];\n delayConfig: DelayConfig;\n};\n\n/** Core streaming logic - consumes source and updates toast, captures return value */\nasync function consumeStream<TYield, TReturn>(\n options: ConsumeStreamOptions<TYield, TReturn>,\n): Promise<ConsumeResult<TYield, TReturn>> {\n const { source: sourceOrFactory, toastId, streaming, onItem, delayConfig } = options;\n // Resolve factory function if needed\n const source = resolveStreamSource(sourceOrFactory);\n\n const items: TYield[] = [];\n let count = 0;\n let isFirst = true;\n\n const updateToast = async (item: TYield) => {\n // Delay before first item (after loading)\n if (isFirst && delayConfig.first) {\n await wait(delayConfig.first);\n isFirst = false;\n } else if (!isFirst && delayConfig.items) {\n // Delay between items (not before first)\n await wait(delayConfig.items);\n } else {\n isFirst = false;\n }\n\n count += 1;\n items.push(item);\n onItem?.(item, count);\n\n const streamingData: StreamingData<TYield> = {\n count,\n latest: item,\n items: [...items],\n };\n const { message: streamingMsg, options: streamingOptions } = await resolveResult(\n streaming,\n streamingData,\n `Streaming... (${count} items)`,\n );\n toast.loading(streamingMsg, { id: toastId, ...streamingOptions });\n };\n\n try {\n // Handle sync Generator specially to capture return value\n if (isGenerator<TYield, TReturn>(source)) {\n let result = source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncGenerator specially to capture return value\n if (isAsyncGenerator<TYield, TReturn>(source)) {\n let result = await source.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await source.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // Handle AsyncChannel specially to capture return value\n if (source instanceof AsyncChannel) {\n const iterator = source[Symbol.asyncIterator]();\n let result = await iterator.next();\n while (!result.done) {\n await updateToast(result.value);\n result = await iterator.next();\n }\n return { ok: true, items, returnValue: result.value };\n }\n\n // For plain iterables/iterators, convert and iterate (no return value)\n const iterable =\n isAsyncIterable(source) || isIterable(source)\n ? toAsyncIterable(source)\n : toAsyncIterable(source as AsyncIterator<TYield>);\n\n for await (const item of iterable) {\n await updateToast(item);\n }\n\n return { ok: true, items, returnValue: undefined as TReturn };\n } catch (error) {\n return { ok: false, items, error };\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStream overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Like `toast.promise` but for iterables/generators/streams.\n *\n * Blocking/awaitable API: waits until the stream completes and resolves to\n * `{ items, returnValue }`. Use this when the caller should wait for all\n * streamed values before continuing.\n *\n * Handles: loading → streaming → success/error.\n *\n * Supports sync and async sources. Captures generator return values.\n * Accepts either an instance or a factory function.\n *\n * @example Async generator with progress and return value\n * ```ts\n * import { toastStream } from \"xantiagoma/sonner\";\n *\n * async function* importUsers() {\n * yield { id: 1, name: \"Ada\" };\n * yield { id: 2, name: \"Grace\" };\n * return { imported: 2 };\n * }\n *\n * const { items, returnValue } = await toastStream(importUsers, {\n * loading: \"Importing users...\",\n * streaming: ({ count, latest }) => `Imported ${count}: ${latest.name}`,\n * success: ({ count, returnValue }) =>\n * `Imported ${count} users (${returnValue.imported} confirmed)`,\n * error: (error, partial) =>\n * `Import failed after ${partial.count} users: ${String(error)}`,\n * });\n * ```\n *\n * @example Sync iterable with artificial delay\n * ```ts\n * const result = await toastStream([\"parse\", \"validate\", \"save\"], {\n * loading: \"Starting...\",\n * streaming: ({ latest }) => `Running ${latest}`,\n * success: ({ count }) => `Completed ${count} steps`,\n * delay: { first: 300, items: 200, result: 150 },\n * });\n * ```\n */\nexport function toastStream<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** Sync Generator with return value */\nexport function toastStream<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncChannel with return type - must come before AsyncIterable overload */\nexport function toastStream<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): Promise<StreamResult<TYield, TReturn>>;\n\n/** AsyncIterable (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterable<TYield> | (() => AsyncIterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** AsyncIterator (no return value) */\nexport function toastStream<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterable (no return value) */\nexport function toastStream<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n/** Sync Iterator (no return value) */\nexport function toastStream<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): Promise<StreamResult<TYield, void>>;\n\n// Implementation\nexport async function toastStream<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): Promise<StreamResult<TYield, TReturn>> {\n const {\n loading,\n streaming,\n success,\n error,\n finally: onFinally,\n onItem,\n delay,\n ...toastOptions\n } = data;\n\n const delayConfig = normalizeDelay(delay);\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n const toastId = toast.loading(loadingMsg, {\n ...toastOptions,\n ...loadingOptions,\n });\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, { id: toastId, ...successOptions });\n return { items: result.items, returnValue: result.returnValue };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// toastStreamAsync overloads\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Return type matching toast.promise's unwrap pattern */\ntype ToastStreamReturn<TYield, TReturn> = (string | number) & {\n unwrap: () => Promise<StreamResult<TYield, TReturn>>;\n};\n\n/**\n * Non-blocking version that returns immediately like `toast.promise`.\n * Returns the toast ID with an `unwrap()` method to await the result later.\n * Accepts either an instance or a factory function.\n *\n * Use this when the UI should continue immediately while the toast tracks the\n * stream in the background.\n *\n * @example\n * ```ts\n * import { toastStreamAsync } from \"xantiagoma/sonner\";\n *\n * const toastId = toastStreamAsync(importUsers, {\n * loading: \"Importing users...\",\n * streaming: ({ count }) => `Imported ${count}`,\n * success: ({ count }) => `Imported ${count} users`,\n * });\n *\n * // The caller continues immediately; await only if/when the result matters.\n * const { items, returnValue } = await toastId.unwrap();\n * ```\n *\n * @example Fire-and-track with error handling\n * ```ts\n * const toastId = toastStreamAsync(uploadFiles(files), {\n * loading: \"Uploading...\",\n * streaming: ({ count }) => `${count} files uploaded`,\n * error: (error, partial) =>\n * `Upload failed after ${partial.count} files: ${String(error)}`,\n * });\n *\n * toastId.unwrap().catch((error) => {\n * reportError(error);\n * });\n * ```\n */\nexport function toastStreamAsync<TYield, TReturn>(\n source:\n | AsyncGenerator<TYield, TReturn, unknown>\n | (() => AsyncGenerator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** Sync Generator with return value */\nexport function toastStreamAsync<TYield, TReturn>(\n source: Generator<TYield, TReturn, unknown> | (() => Generator<TYield, TReturn, unknown>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncChannel with return type */\nexport function toastStreamAsync<TYield, TReturn>(\n source: AsyncChannel<TYield, TReturn> | (() => AsyncChannel<TYield, TReturn>),\n data?: ToastStreamData<TYield, TReturn>,\n): ToastStreamReturn<TYield, TReturn>;\n\n/** AsyncIterable - excludes AsyncChannel which has its own overload */\nexport function toastStreamAsync<TYield>(\n source:\n | Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>\n | (() => Exclude<AsyncIterable<TYield>, AsyncChannel<TYield, unknown>>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** AsyncIterator */\nexport function toastStreamAsync<TYield>(\n source: AsyncIterator<TYield> | (() => AsyncIterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterable */\nexport function toastStreamAsync<TYield>(\n source: Iterable<TYield> | (() => Iterable<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n/** Sync Iterator */\nexport function toastStreamAsync<TYield>(\n source: Iterator<TYield> | (() => Iterator<TYield>),\n data?: ToastStreamData<TYield, void>,\n): ToastStreamReturn<TYield, void>;\n\n// Implementation\nexport function toastStreamAsync<TYield, TReturn = void>(\n source: StreamSource<TYield>,\n data: ToastStreamData<TYield, TReturn> = {},\n): ToastStreamReturn<TYield, TReturn> {\n const { loading, delay, ...rest } = data;\n\n const delayConfig = normalizeDelay(delay);\n // For non-blocking version, show simple loading immediately\n // Callbacks and extended results are resolved inside the promise\n const needsAsyncResolve =\n typeof loading === \"function\" ||\n (typeof loading === \"object\" && loading !== null && \"message\" in loading);\n const initialLoadingMsg = needsAsyncResolve\n ? \"Processing...\"\n : ((loading as ReactNode) ?? \"Processing...\");\n const toastId = toast.loading(initialLoadingMsg);\n\n const promise = (async () => {\n const { streaming, success, error, finally: onFinally, onItem, ...toastOptions } = rest;\n\n // If loading needs async resolution (callback or extended result), resolve and update\n if (needsAsyncResolve) {\n const { message: loadingMsg, options: loadingOptions } = await resolveLoading(\n loading,\n \"Processing...\",\n );\n toast.loading(loadingMsg, { id: toastId, ...loadingOptions });\n }\n\n const result = await consumeStream<TYield, TReturn>({\n source,\n toastId,\n streaming,\n onItem,\n delayConfig,\n });\n\n await onFinally?.();\n\n // Delay before showing result\n if (delayConfig.result) {\n await wait(delayConfig.result);\n }\n\n if (!result.ok) {\n const partial: ErrorPartialData<TYield> = {\n count: result.items.length,\n items: result.items,\n };\n const { message: errorMsg, options: errorOptions } = await resolveErrorResult(\n error,\n result.error,\n partial,\n \"Stream failed\",\n );\n toast.error(errorMsg, { id: toastId, ...toastOptions, ...errorOptions });\n throw result.error;\n }\n\n const successData: SuccessData<TYield, TReturn> = {\n count: result.items.length,\n items: result.items,\n returnValue: result.returnValue,\n };\n const { message: successMsg, options: successOptions } = await resolveResult(\n success,\n successData,\n `Complete! ${result.items.length} items`,\n );\n\n toast.success(successMsg, {\n id: toastId,\n ...toastOptions,\n ...successOptions,\n });\n return { items: result.items, returnValue: result.returnValue };\n })();\n\n // Match toast.promise's return signature\n const res = toastId as ToastStreamReturn<TYield, TReturn>;\n res.unwrap = () => promise;\n return res;\n}\n"],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ # Releasing
2
+
3
+ This package publishes from GitHub Actions when a `v*` tag is pushed.
4
+
5
+ ## CI/CD Overview
6
+
7
+ ### CI
8
+
9
+ `.github/workflows/ci.yml` runs on pushes and pull requests to `main`:
10
+
11
+ - `bun install`
12
+ - `bunx playwright install --with-deps chromium`
13
+ - `bun run lint`
14
+ - `bun run format:check`
15
+ - `npx tsc --noEmit`
16
+ - `bun run test`
17
+ - `bun run build`
18
+
19
+ ### Publish
20
+
21
+ `.github/workflows/publish.yml` runs on pushed tags matching `v*`:
22
+
23
+ - `bun install`
24
+ - `bunx playwright install --with-deps chromium`
25
+ - `bun run test`
26
+ - `bun run build`
27
+ - `npm publish --provenance --access public || npm publish --access public`
28
+ - `gh release create <tag> --generate-notes || echo "Release already exists"`
29
+
30
+ The publish workflow requires `NPM_TOKEN` in repository secrets. GitHub release
31
+ creation uses the workflow `GITHUB_TOKEN`.
32
+
33
+ ## Normal Release Flow
34
+
35
+ 1. Ensure all feature/fix changes are already committed.
36
+ 2. Run local validation:
37
+
38
+ ```bash
39
+ bun run format
40
+ bun run check
41
+ bun run test
42
+ bun run build
43
+ npm pack --dry-run
44
+ ```
45
+
46
+ 3. Create the release commit and tag with an explicit version:
47
+
48
+ ```bash
49
+ bunx changelogen --release -r 0.2.0 --push --no-open
50
+ ```
51
+
52
+ Replace `0.2.0` with the intended version.
53
+
54
+ 4. Watch GitHub Actions:
55
+
56
+ ```bash
57
+ gh run list --limit 5
58
+ gh run watch <run-id>
59
+ ```
60
+
61
+ 5. Confirm npm and GitHub release state:
62
+
63
+ ```bash
64
+ npm view xantiagoma version
65
+ gh release list --limit 5
66
+ ```
67
+
68
+ ## Important: Pre-1.0 Versions
69
+
70
+ Do **not** use `--minor` to create a `0.x` minor release. Changelogen follows
71
+ pre-1.0 semver behavior and may convert a minor bump such as `0.2.0` into the
72
+ next patch, e.g. `0.1.3`.
73
+
74
+ Use an explicit version instead:
75
+
76
+ ```bash
77
+ bunx changelogen --release -r 0.2.0 --push --no-open
78
+ ```
79
+
80
+ The package script `bun run release` uses automatic bump detection. Use it only
81
+ when the inferred version is acceptable. For planned minor releases, prefer the
82
+ explicit `-r <version>` command above.
83
+
84
+ ## If The Wrong Tag Is Pushed
85
+
86
+ If a wrong tag is pushed but npm has **not** published it yet:
87
+
88
+ ```bash
89
+ gh run list --workflow Publish --limit 5
90
+ gh run cancel <publish-run-id>
91
+ git tag -d v0.1.3
92
+ git push origin :refs/tags/v0.1.3
93
+ ```
94
+
95
+ Then create the intended release with an explicit version.
96
+
97
+ If npm has already published the wrong version, do not delete or rewrite it.
98
+ Publish a new corrected version instead.
99
+
100
+ ## Package Contents
101
+
102
+ `package.json` controls published files. Keep `docs` in the `files` list because
103
+ the README links to `docs/PAGINATION.md` and `docs/sync-async-adaptive.md`.
104
+
105
+ Before publishing, verify package contents:
106
+
107
+ ```bash
108
+ npm pack --dry-run --json
109
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xantiagoma",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Lightweight, type-safe TypeScript utilities",
5
5
  "keywords": [
6
6
  "async",