From b2a03b74986233a9c1f69afd9c05a389e592136a Mon Sep 17 00:00:00 2001 From: hw Date: Sat, 8 Aug 2026 17:24:02 +0900 Subject: [PATCH 1/2] fix: escape backslashes in generated frontmatter descriptions escapeYamlString handled quotes and newlines but not backslashes, so the escapeRegExp JSDoc summary produced `\,` inside the YAML description and broke the vitepress website build ("unknown escape sequence"). Escape backslashes first, then quotes. Co-Authored-By: Claude Fable 5 --- .../generate-api-docs/markdown-plugin/src/MarkdownFeature.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/generate-api-docs/markdown-plugin/src/MarkdownFeature.ts b/website/generate-api-docs/markdown-plugin/src/MarkdownFeature.ts index d0c5cffb..53547110 100644 --- a/website/generate-api-docs/markdown-plugin/src/MarkdownFeature.ts +++ b/website/generate-api-docs/markdown-plugin/src/MarkdownFeature.ts @@ -380,7 +380,7 @@ class MarkdownFeature extends MarkdownDocumenterFeature { } private escapeYamlString(str: string): string { - return str.replace(/"/g, '\\"').replace(/\n/g, " "); + return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " "); } } From e9290d8abdba3ffefb1b90c9d3525bb86322fbed Mon Sep 17 00:00:00 2001 From: hw Date: Sat, 8 Aug 2026 17:38:49 +0900 Subject: [PATCH 2/2] docs: add a p-limit/p-map migration guide Semantics verified empirically before writing: concurrentPool matches p-map exactly (pool refill on completion, input-order results, first rejection aborts - 455ms vs 458ms on an uneven-duration benchmark at concurrency 2), while concurrent evaluates in fixed windows (653ms on the same benchmark) and is therefore explicitly not recommended as the p-map replacement. The guide is honest about non-goals: shared ad-hoc limiters, stopOnError: false aggregation, and AbortSignal. Adds the page in en/ko/ja/zh with sidebar entries; vitepress build verified locally. Co-Authored-By: Claude Fable 5 --- website/docs/.vitepress/locales/en.ts | 1 + website/docs/.vitepress/locales/ja.ts | 1 + website/docs/.vitepress/locales/ko.ts | 1 + website/docs/.vitepress/locales/zh.ts | 1 + website/docs/guide/migrate-from-p-map.md | 108 ++++++++++++++++++++ website/docs/ja/guide/migrate-from-p-map.md | 108 ++++++++++++++++++++ website/docs/ko/guide/migrate-from-p-map.md | 106 +++++++++++++++++++ website/docs/zh/guide/migrate-from-p-map.md | 100 ++++++++++++++++++ 8 files changed, 426 insertions(+) create mode 100644 website/docs/guide/migrate-from-p-map.md create mode 100644 website/docs/ja/guide/migrate-from-p-map.md create mode 100644 website/docs/ko/guide/migrate-from-p-map.md create mode 100644 website/docs/zh/guide/migrate-from-p-map.md diff --git a/website/docs/.vitepress/locales/en.ts b/website/docs/.vitepress/locales/en.ts index dda748d1..03a7fce1 100644 --- a/website/docs/.vitepress/locales/en.ts +++ b/website/docs/.vitepress/locales/en.ts @@ -26,6 +26,7 @@ export const enConfig: LocaleSpecificConfig = { }, { text: "Lazy Evaluation", link: "/guide/lazy-evaluation" }, { text: "Handle Concurrency", link: "/guide/handle-concurrency" }, + { text: "Migrate from p-limit/p-map", link: "/guide/migrate-from-p-map" }, { text: "Error Handling", link: "/guide/error-handling" }, { text: "Method Chaining", link: "/guide/method-chaining" }, ], diff --git a/website/docs/.vitepress/locales/ja.ts b/website/docs/.vitepress/locales/ja.ts index fb04c670..ad3ea2ea 100644 --- a/website/docs/.vitepress/locales/ja.ts +++ b/website/docs/.vitepress/locales/ja.ts @@ -30,6 +30,7 @@ export const jaConfig: LocaleSpecificConfig = { }, { text: "遅延評価", link: "/ja/guide/lazy-evaluation" }, { text: "並行処理", link: "/ja/guide/handle-concurrency" }, + { text: "p-limit/p-mapからの移行", link: "/ja/guide/migrate-from-p-map" }, { text: "エラー処理", link: "/ja/guide/error-handling" }, { text: "メソッドチェーン", link: "/ja/guide/method-chaining" }, ], diff --git a/website/docs/.vitepress/locales/ko.ts b/website/docs/.vitepress/locales/ko.ts index c9faa9ad..91254f2d 100644 --- a/website/docs/.vitepress/locales/ko.ts +++ b/website/docs/.vitepress/locales/ko.ts @@ -30,6 +30,7 @@ export const koConfig: LocaleSpecificConfig = { }, { text: "지연 평가", link: "/ko/guide/lazy-evaluation" }, { text: "동시성 처리", link: "/ko/guide/handle-concurrency" }, + { text: "p-limit/p-map에서 마이그레이션", link: "/ko/guide/migrate-from-p-map" }, { text: "에러 처리", link: "/ko/guide/error-handling" }, { text: "메서드 체이닝", link: "/ko/guide/method-chaining" }, ], diff --git a/website/docs/.vitepress/locales/zh.ts b/website/docs/.vitepress/locales/zh.ts index abd251db..0760910c 100644 --- a/website/docs/.vitepress/locales/zh.ts +++ b/website/docs/.vitepress/locales/zh.ts @@ -30,6 +30,7 @@ export const zhConfig: LocaleSpecificConfig = { }, { text: "惰性求值", link: "/zh/guide/lazy-evaluation" }, { text: "并发处理", link: "/zh/guide/handle-concurrency" }, + { text: "从 p-limit/p-map 迁移", link: "/zh/guide/migrate-from-p-map" }, { text: "错误处理", link: "/zh/guide/error-handling" }, { text: "方法链", link: "/zh/guide/method-chaining" }, ], diff --git a/website/docs/guide/migrate-from-p-map.md b/website/docs/guide/migrate-from-p-map.md new file mode 100644 index 00000000..0e32ae71 --- /dev/null +++ b/website/docs/guide/migrate-from-p-map.md @@ -0,0 +1,108 @@ +# Migrate from p-limit / p-map + +If you use `p-map` or `p-limit` to run async work with a concurrency limit over a +collection, FxTS covers the same job with `concurrentPool` — plus streaming, +early termination, and AsyncIterable sources that array-based mappers cannot +express. + +## p-map → concurrentPool + +```typescript +// p-map +import pMap from "p-map"; +const results = await pMap(urls, (url) => fetchItem(url), { concurrency: 5 }); + +// FxTS +import { concurrentPool, map, pipe, toArray, toAsync } from "@fxts/core"; +const results = await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + toArray, +); +``` + +`concurrentPool` has the same semantics as `p-map`: a pool of at most `n` +in-flight tasks where a new task starts as soon as one settles, results in input +order, and the first rejection aborts the run. + +**Pick `concurrentPool`, not `concurrent`, when migrating.** `concurrent` +evaluates in fixed windows of `n` (the next window starts after the previous one +finishes), which is useful for batch semantics but has different throughput +characteristics than a `p-map`-style pool on uneven workloads. + +## p-limit over a collection → concurrentPool + +The common `p-limit` pattern — limiting a mapped collection — is the same +migration: + +```typescript +// p-limit +import pLimit from "p-limit"; +const limit = pLimit(5); +const results = await Promise.all(ids.map((id) => limit(() => loadUser(id)))); + +// FxTS +const results = await pipe( + toAsync(ids), + map((id) => loadUser(id)), + concurrentPool(5), + toArray, +); +``` + +## What does not map + +Being honest about the boundaries: + +- **A shared ad-hoc limiter** — one `limit` instance throttling unrelated call + sites across a codebase — is not what a pipeline library models. Keep + `p-limit` for that. +- **`stopOnError: false`** (collecting an `AggregateError`) has no direct + equivalent; FxTS propagates the first error. To collect outcomes instead, + return result objects from the mapper: + `map(async (url) => fetchItem(url).then((v) => ({ ok: true as const, v }), (e) => ({ ok: false as const, e })))`. +- **`AbortSignal`** is not taken as an option. Laziness covers the usual cases: + stopping consumption stops the pipeline. + +## What you gain over p-map + +Results can be **consumed as a stream** instead of waiting for the whole array: + +```typescript +import { concurrentPool, each, map, pipe, toAsync } from "@fxts/core"; + +await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + each((item) => render(item)), // handled as each result arrives, in order +); +``` + +Pipelines **terminate early** — combine with `take` to stop scheduling work once +you have enough: + +```typescript +import { + concurrentPool, + filter, + map, + pipe, + take, + toArray, + toAsync, +} from "@fxts/core"; + +const firstThree = await pipe( + toAsync(candidates), + map((c) => probe(c)), + concurrentPool(5), + filter((r) => r.alive), + take(3), // stops pulling once three passed - remaining work is not started + toArray, +); +``` + +And the source does not have to be an array — any `AsyncIterable` works +(paginated APIs, streams), so the whole input never needs to be materialized. diff --git a/website/docs/ja/guide/migrate-from-p-map.md b/website/docs/ja/guide/migrate-from-p-map.md new file mode 100644 index 00000000..a86fbb6a --- /dev/null +++ b/website/docs/ja/guide/migrate-from-p-map.md @@ -0,0 +1,108 @@ +# p-limit / p-map からの移行 + +コレクションに同時実行数の制限をかけて非同期処理を実行するために `p-map` や +`p-limit` を使っているなら、FxTS の `concurrentPool` が同じ役割を果たします — +さらに、配列ベースのマッパーでは表現できないストリーミング、早期終了、 +AsyncIterable ソースも得られます。 + +## p-map → concurrentPool + +```typescript +// p-map +import pMap from "p-map"; +const results = await pMap(urls, (url) => fetchItem(url), { concurrency: 5 }); + +// FxTS +import { concurrentPool, map, pipe, toArray, toAsync } from "@fxts/core"; +const results = await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + toArray, +); +``` + +`concurrentPool` は `p-map` と同じセマンティクスです: 最大 `n` 個のタスクが実行中の +プールで、1 つが完了するとすぐ次のタスクが開始され、結果は入力順で返り、最初の失敗が +実行を中断します。 + +**移行の際は `concurrent` ではなく `concurrentPool` を選んでください。** +`concurrent` は `n` 個単位の固定ウィンドウで評価します(前のウィンドウが終わってから +次が始まる)。バッチのセマンティクスが必要な場合には有用ですが、処理時間が不均一な +ワークロードでは `p-map` 方式のプールとスループット特性が異なります。 + +## コレクションに対する p-limit → concurrentPool + +最も一般的な `p-limit` パターン — マッピングされたコレクションへの制限 — も同じ +方法で移行できます: + +```typescript +// p-limit +import pLimit from "p-limit"; +const limit = pLimit(5); +const results = await Promise.all(ids.map((id) => limit(() => loadUser(id)))); + +// FxTS +const results = await pipe( + toAsync(ids), + map((id) => loadUser(id)), + concurrentPool(5), + toArray, +); +``` + +## 対応しないもの + +境界を正直に述べると: + +- **共有される ad-hoc リミッター** — 1 つの `limit` インスタンスでコードベース各所の + 無関係な呼び出しを制限するパターン — はパイプラインライブラリがモデル化する対象では + ありません。その用途には `p-limit` を使い続けてください。 +- **`stopOnError: false`**(`AggregateError` への収集)の直接の対応物はありません。 + FxTS は最初のエラーを伝播します。結果を収集したい場合はマッパーで結果オブジェクトを + 返してください: + `map(async (url) => fetchItem(url).then((v) => ({ ok: true as const, v }), (e) => ({ ok: false as const, e })))`. +- **`AbortSignal`** オプションはありません。一般的なケースは遅延評価がカバーします: + 消費を止めればパイプラインも止まります。 + +## p-map に対して得られるもの + +配列全体を待たずに、結果を**ストリームとして消費**できます: + +```typescript +import { concurrentPool, each, map, pipe, toAsync } from "@fxts/core"; + +await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + each((item) => render(item)), // 結果が届くたびに、順序どおりに処理 +); +``` + +パイプラインは**早期終了**します — `take` と組み合わせれば、十分な数が揃った時点で +タスクのスケジューリング自体が止まります: + +```typescript +import { + concurrentPool, + filter, + map, + pipe, + take, + toArray, + toAsync, +} from "@fxts/core"; + +const firstThree = await pipe( + toAsync(candidates), + map((c) => probe(c)), + concurrentPool(5), + filter((r) => r.alive), + take(3), // 3 つ得られたら取得を止め、残りの処理は開始されません + toArray, +); +``` + +そしてソースは配列である必要はありません — どんな `AsyncIterable` でも動作するため +(ページネーション API、ストリーム)、入力全体をメモリに実体化する必要がありません。 diff --git a/website/docs/ko/guide/migrate-from-p-map.md b/website/docs/ko/guide/migrate-from-p-map.md new file mode 100644 index 00000000..9f7d2c13 --- /dev/null +++ b/website/docs/ko/guide/migrate-from-p-map.md @@ -0,0 +1,106 @@ +# p-limit / p-map에서 마이그레이션 + +컬렉션에 동시성 제한을 걸어 비동기 작업을 실행하기 위해 `p-map`이나 `p-limit`을 +사용하고 있다면, FxTS의 `concurrentPool`이 같은 일을 해냅니다 — 여기에 배열 기반 +매퍼로는 표현할 수 없는 스트리밍, 조기 종료, AsyncIterable 소스까지 더해집니다. + +## p-map → concurrentPool + +```typescript +// p-map +import pMap from "p-map"; +const results = await pMap(urls, (url) => fetchItem(url), { concurrency: 5 }); + +// FxTS +import { concurrentPool, map, pipe, toArray, toAsync } from "@fxts/core"; +const results = await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + toArray, +); +``` + +`concurrentPool`은 `p-map`과 동일한 의미론을 가집니다: 최대 `n`개의 작업이 실행 +중인 풀에서 하나가 끝나는 즉시 다음 작업이 시작되고, 결과는 입력 순서대로 +반환되며, 첫 번째 실패가 실행을 중단시킵니다. + +**마이그레이션할 때는 `concurrent`가 아니라 `concurrentPool`을 선택하세요.** +`concurrent`는 `n`개 단위의 고정 윈도우로 평가하며(이전 윈도우가 끝나야 다음 +윈도우가 시작), 배치 의미론이 필요할 때는 유용하지만 작업 시간이 불균일한 +워크로드에서는 `p-map` 방식의 풀과 처리량 특성이 다릅니다. + +## 컬렉션에 대한 p-limit → concurrentPool + +가장 흔한 `p-limit` 패턴 — 매핑된 컬렉션에 제한을 거는 것 — 도 같은 방식으로 +마이그레이션됩니다: + +```typescript +// p-limit +import pLimit from "p-limit"; +const limit = pLimit(5); +const results = await Promise.all(ids.map((id) => limit(() => loadUser(id)))); + +// FxTS +const results = await pipe( + toAsync(ids), + map((id) => loadUser(id)), + concurrentPool(5), + toArray, +); +``` + +## 대응되지 않는 것 + +경계를 정직하게 말하면: + +- **공유되는 ad-hoc 리미터** — 하나의 `limit` 인스턴스로 코드베이스 곳곳의 서로 + 무관한 호출을 제한하는 패턴 — 는 파이프라인 라이브러리가 모델링하는 대상이 + 아닙니다. 그 용도로는 `p-limit`을 유지하세요. +- **`stopOnError: false`** (`AggregateError`로 수집)의 직접 대응물은 없습니다. + FxTS는 첫 에러를 전파합니다. 결과를 수집하려면 매퍼에서 결과 객체를 반환하세요: + `map(async (url) => fetchItem(url).then((v) => ({ ok: true as const, v }), (e) => ({ ok: false as const, e })))`. +- **`AbortSignal`** 옵션은 받지 않습니다. 일반적인 경우는 lazy 특성이 커버합니다: + 소비를 멈추면 파이프라인도 멈춥니다. + +## p-map 대비 얻는 것 + +전체 배열을 기다리지 않고 결과를 **스트림으로 소비**할 수 있습니다: + +```typescript +import { concurrentPool, each, map, pipe, toAsync } from "@fxts/core"; + +await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + each((item) => render(item)), // 결과가 도착하는 대로, 순서대로 처리 +); +``` + +파이프라인은 **조기 종료**됩니다 — `take`와 결합하면 충분히 모인 시점에 작업 +스케줄링 자체가 멈춥니다: + +```typescript +import { + concurrentPool, + filter, + map, + pipe, + take, + toArray, + toAsync, +} from "@fxts/core"; + +const firstThree = await pipe( + toAsync(candidates), + map((c) => probe(c)), + concurrentPool(5), + filter((r) => r.alive), + take(3), // 3개를 얻으면 당기기를 멈추고, 남은 작업은 시작되지 않습니다 + toArray, +); +``` + +그리고 소스가 배열일 필요가 없습니다 — 어떤 `AsyncIterable`이든 동작하므로 +(페이지네이션 API, 스트림), 전체 입력을 메모리에 실체화할 필요가 없습니다. diff --git a/website/docs/zh/guide/migrate-from-p-map.md b/website/docs/zh/guide/migrate-from-p-map.md new file mode 100644 index 00000000..d3234088 --- /dev/null +++ b/website/docs/zh/guide/migrate-from-p-map.md @@ -0,0 +1,100 @@ +# 从 p-limit / p-map 迁移 + +如果你使用 `p-map` 或 `p-limit` 对集合施加并发限制来执行异步任务,FxTS 的 +`concurrentPool` 可以完成同样的工作 — 并且还提供数组式 mapper 无法表达的流式消费、 +提前终止和 AsyncIterable 数据源。 + +## p-map → concurrentPool + +```typescript +// p-map +import pMap from "p-map"; +const results = await pMap(urls, (url) => fetchItem(url), { concurrency: 5 }); + +// FxTS +import { concurrentPool, map, pipe, toArray, toAsync } from "@fxts/core"; +const results = await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + toArray, +); +``` + +`concurrentPool` 与 `p-map` 语义相同:一个最多 `n` 个任务同时执行的池,其中一个任务 +完成后立即开始下一个,结果按输入顺序返回,第一个失败会中止整个执行。 + +**迁移时请选择 `concurrentPool` 而不是 `concurrent`。** `concurrent` 以 `n` 个为 +单位的固定窗口求值(前一个窗口结束后下一个才开始),需要批处理语义时很有用,但在任务 +耗时不均匀的场景下,其吞吐特性与 `p-map` 式的池不同。 + +## 针对集合的 p-limit → concurrentPool + +最常见的 `p-limit` 模式 — 对映射后的集合施加限制 — 也是同样的迁移方式: + +```typescript +// p-limit +import pLimit from "p-limit"; +const limit = pLimit(5); +const results = await Promise.all(ids.map((id) => limit(() => loadUser(id)))); + +// FxTS +const results = await pipe( + toAsync(ids), + map((id) => loadUser(id)), + concurrentPool(5), + toArray, +); +``` + +## 不对应的部分 + +坦率地说明边界: + +- **共享的 ad-hoc 限流器** — 用一个 `limit` 实例限制代码库中互不相关的调用 — 不是 + 管道库所建模的对象。这种用途请继续使用 `p-limit`。 +- **`stopOnError: false`**(收集为 `AggregateError`)没有直接对应。FxTS 会传播第一个 + 错误。如需收集结果,请在 mapper 中返回结果对象: + `map(async (url) => fetchItem(url).then((v) => ({ ok: true as const, v }), (e) => ({ ok: false as const, e })))`. +- 不接受 **`AbortSignal`** 选项。惰性求值覆盖了常见场景:停止消费,管道就会停止。 + +## 相比 p-map 的收益 + +结果可以**以流的方式消费**,无需等待整个数组: + +```typescript +import { concurrentPool, each, map, pipe, toAsync } from "@fxts/core"; + +await pipe( + toAsync(urls), + map((url) => fetchItem(url)), + concurrentPool(5), + each((item) => render(item)), // 每个结果到达时按顺序处理 +); +``` + +管道支持**提前终止** — 与 `take` 结合,一旦收集到足够的结果就停止调度后续任务: + +```typescript +import { + concurrentPool, + filter, + map, + pipe, + take, + toArray, + toAsync, +} from "@fxts/core"; + +const firstThree = await pipe( + toAsync(candidates), + map((c) => probe(c)), + concurrentPool(5), + filter((r) => r.alive), + take(3), // 拿到 3 个后停止拉取,剩余任务不会启动 + toArray, +); +``` + +而且数据源不必是数组 — 任何 `AsyncIterable` 都可以(分页 API、流),因此永远不需要 +把全部输入物化到内存中。