Skip to content

Commit 8a76d1a

Browse files
committed
Add cancellable runtime-neutral async XLSX inserts
1 parent 7181b55 commit 8a76d1a

21 files changed

Lines changed: 820 additions & 17 deletions

‎Cargo.lock‎

Lines changed: 81 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@ homepage = "https://github.com/mini-software/MiniExcel-Rust"
1212

1313
[workspace.dependencies]
1414
atomicwrites = "0.4.4"
15+
async-channel = "2.5.0"
1516
calamine = { version = "0.35.0", features = ["chrono"] }
1617
chrono = { version = "0.4.42", features = ["serde"] }
1718
clap = { version = "4.6.6", features = ["derive"] }
1819
console_error_panic_hook = "0.1.7"
20+
event-listener = "5.4.1"
1921
fs2 = "0.4.3"
22+
futures-core = "0.3.31"
23+
futures-executor = "0.3.31"
24+
futures-util = { version = "0.3.31", default-features = false, features = ["async-await", "async-await-macro", "std"] }
2025
indexmap = { version = "2.12.1", features = ["serde"] }
2126
quick-xml = "0.39.4"
2227
rust_xlsxwriter = { version = "0.96.0", features = ["chrono", "serde"] }

‎README.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,29 @@ Insert holds a cross-process advisory lock and verifies a source SHA-256 fingerp
314314
concurrent writers or external source changes return a deterministic conflict instead of silently
315315
overwriting newer content.
316316

317+
Enable the optional `async` feature to feed an existing-workbook Insert from a runtime-neutral
318+
`Stream<Item = miniexcel::Result<DynamicRow>>`:
319+
320+
```rust
321+
use miniexcel::{CancellationToken, InsertOptions, MiniExcel};
322+
323+
let cancellation = CancellationToken::new();
324+
let count = MiniExcel::insert_with_schema_async_with_cancellation(
325+
"book.xlsx",
326+
&["Name".to_owned(), "Version".to_owned()],
327+
rows,
328+
&InsertOptions::new().with_sheet_name("Async"),
329+
cancellation,
330+
).await?;
331+
```
332+
333+
The async API uses a bounded channel for row backpressure and isolates blocking XLSX work on a
334+
dedicated thread; ZIP, XML, and filesystem I/O are not async. Explicit cancellation waits for
335+
worker cleanup before returning. Dropping the future requests cooperative cancellation and cleanup
336+
finishes in the background. Cancellation that wins before commit preserves the original workbook;
337+
once atomic replacement begins, cancellation cannot revoke it. No async runtime is enabled by
338+
default, and Tokio is not a dependency.
339+
317340
Appending a formula-free worksheet preserves an existing calculation chain and workbook calculation properties. Replacement removes the complete stale `calcChain` part, relationship, and content-type override, then sets `fullCalcOnLoad` and `forceFullCalc` so Excel recalculates on the next open. MiniExcel does not evaluate or rewrite formulas; formulas and cached values in untouched worksheets remain byte-identical.
318341

319342
## Typed Writing

‎README.zh-CN.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,28 @@ XML depth 与 relationship count;同时拒绝不安全或别名 part path、
312312
lock,并在 commit 前校验 source SHA-256 fingerprint;并发 writer 或外部 source 变化会返回
313313
确定性 conflict,不会静默覆盖更新后的内容。
314314

315+
启用可选 `async` feature 后,可通过 runtime-neutral
316+
`Stream<Item = miniexcel::Result<DynamicRow>>` 为现有 workbook 的 Insert 提供 row:
317+
318+
```rust
319+
use miniexcel::{CancellationToken, InsertOptions, MiniExcel};
320+
321+
let cancellation = CancellationToken::new();
322+
let count = MiniExcel::insert_with_schema_async_with_cancellation(
323+
"book.xlsx",
324+
&["Name".to_owned(), "Version".to_owned()],
325+
rows,
326+
&InsertOptions::new().with_sheet_name("Async"),
327+
cancellation,
328+
).await?;
329+
```
330+
331+
Async API 使用 bounded channel 提供 row backpressure,并把 blocking XLSX 工作隔离到专用
332+
thread;ZIP、XML 和 filesystem I/O 并不是 async。显式 cancellation 会等待 worker cleanup
333+
后返回;drop future 会请求 cooperative cancellation,cleanup 在后台完成。Commit 前由
334+
cancellation 获胜时原 workbook 保持不变;atomic replacement 开始后不能撤销。默认不启用
335+
任何 async runtime,也不依赖 Tokio。
336+
315337
追加 formula-free worksheet 时会保留已有 calculation chain 与 workbook calculation property。Replacement 会完整删除 stale `calcChain` part、relationship 和 content-type override,并设置 `fullCalcOnLoad` 与 `forceFullCalc`,让 Excel 下次打开时重算。MiniExcel 不执行或改写公式;未修改 worksheet 中的 formula 与 cached value 保持原始字节。
316338

317339
## 类型化写入

‎docs/compatibility.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The Rust MVP implements the smallest useful MiniExcel-style XLSX read/write surf
1111
| Dependency | Locked API line | Role | License | MSRV note |
1212
| --- | --- | --- | --- | --- |
1313
| `atomicwrites` | 0.4 | Safe cross-platform atomic replacement for Windows path inserts | MIT | Windows-only dependency; checked with Rust 1.85 |
14+
| `async-channel` / `event-listener` / `futures-*` | 2.5 / 5.4 / 0.3 | Optional runtime-neutral async Insert producer and cancellation | MIT OR Apache-2.0 | Enabled only by the `async` feature; checked with Rust 1.85 |
1415
| `fs2` | 0.4 | Cross-platform advisory locking for path inserts | MIT OR Apache-2.0 | Checked with Rust 1.85 |
1516
| `calamine` | 0.35 | XLSX parsing and Serde row deserialization | MIT | 0.35 declares Rust 1.83 |
1617
| `clap` | 4.6 | Local CLI argument parsing | MIT OR Apache-2.0 | 4.6 declares Rust 1.85 |
@@ -49,6 +50,7 @@ The latest `calamine 0.36` and `rust_xlsxwriter 0.97` require Rust 1.88. The MVP
4950
| Typed export | `save_as_serialized<T>()` | Uses Serde mapping internally |
5051
| Multi-sheet export | `save_as_sheets()` / `save_as_serialized_sheets()` | Preserves input sheet order and returns data-row counts |
5152
| `InsertSheet` append/replace | `insert()` / `insert_with_schema()` / `insert_serialized()` / borrowed reader-to-writer variants | Path APIs are atomic; separate borrowed streams require an empty sink and preserve package behavior without atomic commit |
53+
| Async Insert producer | `insert_with_schema_async*()` | Optional `async` feature; bounded producer channel with blocking XLSX work on a dedicated thread |
5254
| Per-sheet visibility | `WriteOptions::with_sheet_visibility()` | Visible, hidden, and very hidden; first visible sheet is active |
5355
| `overwriteFile` | `WriteOptions::with_overwrite_file()` | Defaults to `false`; existing paths require explicit opt-in |
5456
| `FreezeRowCount` / `FreezeColumnCount` | `WriteOptions::with_freeze_row_count()` / `with_freeze_column_count()` | Defaults to one frozen row and zero frozen columns |

‎docs/compatibility.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Rust MVP 在统一的 `MiniExcel` facade 后实现最小但实用的 MiniExcel
1111
| 依赖 | 锁定 API 版本线 | 用途 | 许可证 | MSRV 说明 |
1212
| --- | --- | --- | --- | --- |
1313
| `atomicwrites` | 0.4 | Windows path insert 的安全跨平台原子替换 | MIT | 仅 Windows 依赖;已使用 Rust 1.85 检查 |
14+
| `async-channel` / `event-listener` / `futures-*` | 2.5 / 5.4 / 0.3 | 可选 runtime-neutral async Insert producer 与 cancellation | MIT OR Apache-2.0 | 仅由 `async` feature 启用;已使用 Rust 1.85 检查 |
1415
| `fs2` | 0.4 | Path insert 的跨平台 advisory lock | MIT OR Apache-2.0 | 已使用 Rust 1.85 检查 |
1516
| `calamine` | 0.35 | XLSX 解析和 Serde row 反序列化 | MIT | 0.35 声明 Rust 1.83 |
1617
| `clap` | 4.6 | 本地 CLI 参数解析 | MIT OR Apache-2.0 | 4.6 声明 Rust 1.85 |
@@ -49,6 +50,7 @@ Rust MVP 在统一的 `MiniExcel` facade 后实现最小但实用的 MiniExcel
4950
| 类型化导出 | `save_as_serialized<T>()` | 内部使用 Serde 映射 |
5051
| 多工作表导出 | `save_as_sheets()` / `save_as_serialized_sheets()` | 保留输入工作表顺序并返回数据行数 |
5152
| `InsertSheet` append/replace | `insert()` / `insert_with_schema()` / `insert_serialized()` / borrowed reader-to-writer variants | Path API 为原子操作;独立 borrowed stream 要求空 sink,并在无原子 commit 的情况下保持相同 package 行为 |
53+
| Async Insert producer | `insert_with_schema_async*()` | 可选 `async` feature;bounded producer channel,XLSX 工作在专用 blocking thread |
5254
| 每表 visibility | `WriteOptions::with_sheet_visibility()` | visible、hidden、very hidden;第一个 visible sheet 为 active |
5355
| `overwriteFile` | `WriteOptions::with_overwrite_file()` | 默认 `false`;已有路径需要显式允许覆盖 |
5456
| `FreezeRowCount` / `FreezeColumnCount` | `WriteOptions::with_freeze_row_count()` / `with_freeze_column_count()` | 默认冻结一行、零列 |

‎docs/dotnet-feature-gaps.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
3030
| Named tables | Missing | Query an OpenXML table by table name (`QueryTable`), including table-specific headers and bounds. |
3131
| DataReader and DataTable | Missing | `IDataReader`/async reader access, schema tables, typed getters, `NextResult` sheet traversal, and `DataTable` materialization. |
3232
| Caller-owned streams | Partial | Borrowed synchronous dynamic/typed/structured visitors, metadata reads, dynamic/schema/typed/multi-sheet writers, and separate reader-to-writer Insert are implemented with leave-open semantics. Borrowed lazy iterators, async streams, and template streams remain unsupported. |
33-
| Async and cancellation | Missing | Async query/export/template operations, async row sources, cancellation tokens, and progress callbacks. Rust iterators are synchronous even though path reads use a worker thread. |
33+
| Async and cancellation | Partial | Optional runtime-neutral async row production and cancellation are implemented for explicit-schema path Insert. Async query/export/template operations, typed async sources, and progress callbacks remain unsupported. ZIP and filesystem work remains blocking on a dedicated worker. |
3434
| General save inputs | Partial | Export from general objects/enumerables, dictionaries, `DataTable`, `IDataReader`, and async enumerables, with progress. Rust accepts dynamic or same-type Serde slices and reports per-sheet row counts. |
3535
| Multi-sheet export | Partial | Rust creates ordered visible, hidden, and very-hidden worksheets, but does not yet accept heterogeneous Serde row types in one call. |
3636
| Existing-workbook operations | Partial | Rust appends or strictly replaces worksheets through atomic path APIs or separate borrowed streams, preserving unrelated package parts and worksheet identity. Copy-and-add, rename, reorder, and standalone visibility mutation remain unsupported. |

‎docs/dotnet-feature-gaps.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
3030
| 命名表格 | 未实现 | 按表格名称查询 OpenXML Table(`QueryTable`),并遵循表格自身的表头与范围。 |
3131
| DataReader 与 DataTable | 未实现 | `IDataReader`/异步 reader、架构表、强类型 getter、通过 `NextResult` 遍历工作表,以及物化为 `DataTable`。 |
3232
| 调用方提供的流 | 部分实现 | 已实现借用的同步动态/类型化/structured visitor、metadata 读取、动态/schema/类型化/多表 writer,以及独立 reader-to-writer Insert,并保持 leave-open。借用 lazy iterator、async stream 和 template stream 仍不支持。 |
33-
| 异步与取消 | 未实现 | 异步查询/导出/模板操作、异步行源、取消令牌和进度回调。Rust 路径读取虽使用工作线程,对外迭代器仍是同步的。 |
33+
| 异步与取消 | 部分实现 | 显式 schema path Insert 已支持可选、runtime-neutral 的 async row producer 与 cancellation。Async query/export/template、类型化 async source 和 progress callback 仍不支持;ZIP 与 filesystem 工作仍在专用 blocking worker 上执行。 |
3434
| 通用保存输入 | 部分实现 | 从普通对象/可枚举对象、字典、`DataTable`、`IDataReader` 和异步枚举导出,并报告进度。Rust 接受动态行或同类型 Serde 切片,并返回每张工作表的行数。 |
3535
| 多工作表导出 | 部分实现 | Rust 可按输入顺序创建 visible、hidden 和 very-hidden 工作表,但尚不能在一次调用中接受异构 Serde 行类型。 |
3636
| 修改现有工作簿 | 部分实现 | Rust 可通过原子 path API 或独立 borrowed stream append 或严格 replace worksheet,并保留无关 package part 与 worksheet identity。复制新增、重命名、重排和独立 visibility 修改仍不支持。 |

‎docs/insert-existing-workbook-plan.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -406,17 +406,23 @@ successfully round-tripped the streamed style-rebase output.
406406

407407
Depends on Task 11. This task is optional and should use a feature flag.
408408

409-
- [ ] Define an async producer API without making Tokio a mandatory core dependency.
410-
- [ ] Feed a blocking package worker through a bounded channel.
411-
- [ ] Support cancellation before preflight, during row generation, during ZIP copy, and before commit.
412-
- [ ] Guarantee cleanup and original-file preservation for every cancellation point.
413-
- [ ] Do not present async scheduling as async ZIP I/O if the ZIP backend remains blocking.
409+
- [x] Define an async producer API without making Tokio a mandatory core dependency.
410+
- [x] Feed a blocking package worker through a bounded channel.
411+
- [x] Support cancellation before preflight, during row generation, during ZIP copy, and before commit.
412+
- [x] Guarantee cleanup and original-file preservation for every cancellation point.
413+
- [x] Do not present async scheduling as async ZIP I/O if the ZIP backend remains blocking.
414414

415415
Acceptance:
416416

417417
- Cancellation tests cover all lifecycle phases.
418418
- Default feature builds remain runtime-neutral.
419419

420+
Completed on 2026-08-25 behind the optional `async` feature. The public explicit-schema async
421+
producer uses a capacity-16 channel, a runtime-neutral `CancellationToken`, and one blocking XLSX
422+
worker thread. Deterministic phase-hook tests cover cancellation before preflight, before row
423+
polling, during row generation, ZIP copy, validation, and before commit, plus future-drop cleanup
424+
and producer-error over-poll protection. Default and wasm32 builds remain runtime-neutral.
425+
420426
### Task 13: Documentation, Compatibility, And Release
421427

422428
Depends on all required tasks above.

‎docs/insert-existing-workbook-plan.zh-CN.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -405,17 +405,23 @@ roundtrip 流式 style-rebase output。
405405

406406
依赖 Task 11;此 task 可选,并应使用 feature flag。
407407

408-
- [ ] 定义 async producer API,不让 Tokio 成为 core mandatory dependency。
409-
- [ ] 通过 bounded channel 将数据交给 blocking package worker。
410-
- [ ] 支持 preflight 前、row generation、ZIP copy 与 commit 前 cancellation。
411-
- [ ] 每个 cancellation point 都保证 cleanup 和原文件保留。
412-
- [ ] 如果 ZIP backend 仍是 blocking,不得把 async scheduling 描述为 async ZIP I/O。
408+
- [x] 定义 async producer API,不让 Tokio 成为 core mandatory dependency。
409+
- [x] 通过 bounded channel 将数据交给 blocking package worker。
410+
- [x] 支持 preflight 前、row generation、ZIP copy 与 commit 前 cancellation。
411+
- [x] 每个 cancellation point 都保证 cleanup 和原文件保留。
412+
- [x] 如果 ZIP backend 仍是 blocking,不得把 async scheduling 描述为 async ZIP I/O。
413413

414414
验收:
415415

416416
- Cancellation test 覆盖全部生命周期阶段。
417417
- 默认 feature build 保持 runtime-neutral。
418418

419+
已于 2026-08-25 在可选 `async` feature 后完成。Public 显式 schema async producer 使用
420+
capacity-16 channel、runtime-neutral `CancellationToken` 和一个 blocking XLSX worker
421+
thread。确定性 phase-hook test 覆盖 preflight 前、row polling 前、row generation、ZIP
422+
copy、validation、commit 前 cancellation,以及 future-drop cleanup 与 producer-error
423+
over-poll 防护。默认与 wasm32 build 仍保持 runtime-neutral。
424+
419425
### Task 13:文档、兼容契约与 Release
420426

421427
依赖以上全部必需 task。

0 commit comments

Comments
 (0)