@thru/sdk 0.2.22

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.
Files changed (73) hide show
  1. package/README.abi.md +112 -0
  2. package/README.md +342 -0
  3. package/dist/abi/index.cjs +946 -0
  4. package/dist/abi/index.cjs.map +1 -0
  5. package/dist/abi/index.d.cts +331 -0
  6. package/dist/abi/index.d.ts +331 -0
  7. package/dist/abi/index.js +892 -0
  8. package/dist/abi/index.js.map +1 -0
  9. package/dist/abi/wasm/bundler/abi_reflect_wasm.d.ts +65 -0
  10. package/dist/abi/wasm/bundler/abi_reflect_wasm.js +5 -0
  11. package/dist/abi/wasm/bundler/abi_reflect_wasm_bg.js +412 -0
  12. package/dist/abi/wasm/bundler/abi_reflect_wasm_bg.wasm +0 -0
  13. package/dist/abi/wasm/bundler/abi_reflect_wasm_bg.wasm.d.ts +24 -0
  14. package/dist/abi/wasm/bundler/package.json +17 -0
  15. package/dist/abi/wasm/node/abi_reflect_wasm.d.ts +65 -0
  16. package/dist/abi/wasm/node/abi_reflect_wasm.js +425 -0
  17. package/dist/abi/wasm/node/abi_reflect_wasm_bg.wasm +0 -0
  18. package/dist/abi/wasm/node/abi_reflect_wasm_bg.wasm.d.ts +24 -0
  19. package/dist/abi/wasm/node/package.json +11 -0
  20. package/dist/abi/wasm/web/abi_reflect_wasm.d.ts +114 -0
  21. package/dist/abi/wasm/web/abi_reflect_wasm.js +499 -0
  22. package/dist/abi/wasm/web/abi_reflect_wasm_bg.wasm +0 -0
  23. package/dist/abi/wasm/web/abi_reflect_wasm_bg.wasm.d.ts +24 -0
  24. package/dist/abi/wasm/web/package.json +15 -0
  25. package/dist/chunk-CVBQ4UPL.js +3614 -0
  26. package/dist/chunk-CVBQ4UPL.js.map +1 -0
  27. package/dist/chunk-NSBPE2FW.js +15 -0
  28. package/dist/chunk-NSBPE2FW.js.map +1 -0
  29. package/dist/chunk-RL3Q6FDE.js +105 -0
  30. package/dist/chunk-RL3Q6FDE.js.map +1 -0
  31. package/dist/chunk-UDIXYJXC.js +260 -0
  32. package/dist/chunk-UDIXYJXC.js.map +1 -0
  33. package/dist/chunk-YQDWOMNO.js +271 -0
  34. package/dist/chunk-YQDWOMNO.js.map +1 -0
  35. package/dist/client-Cfi7zRLm.d.cts +1247 -0
  36. package/dist/client-worIo0a3.d.ts +1247 -0
  37. package/dist/client.cjs +3791 -0
  38. package/dist/client.cjs.map +1 -0
  39. package/dist/client.d.cts +7 -0
  40. package/dist/client.d.ts +7 -0
  41. package/dist/client.js +7 -0
  42. package/dist/client.js.map +1 -0
  43. package/dist/crypto/index.cjs +170 -0
  44. package/dist/crypto/index.cjs.map +1 -0
  45. package/dist/crypto/index.d.cts +60 -0
  46. package/dist/crypto/index.d.ts +60 -0
  47. package/dist/crypto/index.js +5 -0
  48. package/dist/crypto/index.js.map +1 -0
  49. package/dist/helpers/index.cjs +270 -0
  50. package/dist/helpers/index.cjs.map +1 -0
  51. package/dist/helpers/index.d.cts +15 -0
  52. package/dist/helpers/index.d.ts +15 -0
  53. package/dist/helpers/index.js +4 -0
  54. package/dist/helpers/index.js.map +1 -0
  55. package/dist/metafile-cjs.json +1 -0
  56. package/dist/metafile-esm.json +1 -0
  57. package/dist/proto/index.cjs +433 -0
  58. package/dist/proto/index.cjs.map +1 -0
  59. package/dist/proto/index.d.cts +435 -0
  60. package/dist/proto/index.d.ts +435 -0
  61. package/dist/proto/index.js +4 -0
  62. package/dist/proto/index.js.map +1 -0
  63. package/dist/sdk.cjs +4137 -0
  64. package/dist/sdk.cjs.map +1 -0
  65. package/dist/sdk.d.cts +36 -0
  66. package/dist/sdk.d.ts +36 -0
  67. package/dist/sdk.js +7 -0
  68. package/dist/sdk.js.map +1 -0
  69. package/dist/streaming_service_pb-DvCdJiCr.d.cts +4877 -0
  70. package/dist/streaming_service_pb-DvCdJiCr.d.ts +4877 -0
  71. package/dist/webcrypto-LTajLAad.d.cts +3 -0
  72. package/dist/webcrypto-LTajLAad.d.ts +3 -0
  73. package/package.json +88 -0
package/README.abi.md ADDED
@@ -0,0 +1,112 @@
1
+ # @thru/sdk/abi – WASM-backed ABI reflection
2
+
3
+ This package is now a thin TypeScript wrapper around the Rust `abi_reflect`
4
+ runtime. We compile the Rust crate to WebAssembly, ship both Node + bundler
5
+ targets, and expose a small async API for reflecting ABI YAML + binary payloads.
6
+ All layout math and validation run inside the Rust engine so TypeScript stays
7
+ lightweight and automatically inherits IR upgrades.
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { ensureWasmLoaded, formatReflection, reflect } from "@thru/sdk/abi";
15
+ import tokenAbi from "./abi/token_program.abi.yaml?raw";
16
+
17
+ async function example() {
18
+ await ensureWasmLoaded(); // formatter + reflector live inside WASM
19
+
20
+ const payload = new Uint8Array([0x01, 0, 0, 0, 0, 0, 0, 0]);
21
+ const reflection = await reflect(tokenAbi, "TokenInstruction", {
22
+ type: "binary",
23
+ value: payload,
24
+ });
25
+
26
+ console.log(reflection.value); // JSON emitted by abi_reflect
27
+
28
+ // Collapse the verbose JSON tree into something human-readable
29
+ const formatted = formatReflection(reflection);
30
+ console.log(formatted.value.payload.variant); // "initialize_mint"
31
+ }
32
+
33
+ example();
34
+ ```
35
+
36
+ * The ABI text **must already be flattened** (imports resolved). The Rust
37
+ resolver enforces this.
38
+ * Results are JSON blobs straight from `serde_json`. They include the full type
39
+ info + value trees used by the CLI tooling.
40
+
41
+ ---
42
+
43
+ ## Public API
44
+
45
+ | Function | Description |
46
+ | --- | --- |
47
+ | `reflect(abi: string, typeName: string, payload: { type: "binary", value: BinaryLike } \| { type: "hex", value: string })` | Reflects binary data (or hex) and returns the parsed JSON payload. |
48
+ | `formatReflection(raw: JsonValue)` | Delegates to the WASM formatter to collapse verbose JSON trees. Requires `ensureWasmLoaded()` (or any prior call to `reflect`) before use. |
49
+ | `buildLayoutIr(abi: string)` | Runs the shared Layout IR builder and returns the serialized IR document (schema version, per-type expressions, parameters). |
50
+ | `ensureWasmLoaded()` | Preloads the WASM bindings for callers that want to pay the initialization cost up-front. `reflect` calls it lazily. |
51
+
52
+ All helpers are async, because loading + instantiating the WASM module can touch
53
+ the filesystem (Node) or issue dynamic imports (bundlers).
54
+
55
+ ---
56
+
57
+ ## WASM workflow
58
+
59
+ The generated artifacts are staged under `web/packages/sdk/wasm/abi/{bundler,node,web}`
60
+ before package builds. They are ignored in git, but copied into `dist/abi/wasm`
61
+ so published packages work without a local Rust toolchain. When `abi_reflect` or
62
+ the shared IR changes, rebuild everything with:
63
+
64
+ ```bash
65
+ # From repo root
66
+ pnpm --filter @thru/sdk run build:wasm
67
+ ```
68
+
69
+ That script runs `wasm-pack build` three times (bundler + node + web targets) inside
70
+ `rpc/abi/abi_reflect_wasm`, then copies the fresh outputs into
71
+ `web/packages/sdk/wasm/abi`. The regular `pnpm --filter @thru/sdk build` step runs
72
+ `tsup` and copies those WASM folders into `dist/abi/wasm` so published packages
73
+ resolve the dynamic imports automatically.
74
+
75
+ When developing inside the monorepo, Vitest loads the TypeScript sources
76
+ directly. The runtime detects when it is executing from `src/` and reaches for
77
+ `../wasm`, so make sure the synced artifacts exist before running the tests.
78
+
79
+ ---
80
+
81
+ ## Testing
82
+
83
+ ```bash
84
+ pnpm --filter @thru/sdk test
85
+ ```
86
+
87
+ Vitest exercises both `reflectHex` and `reflectBinary` against the
88
+ `SimpleStruct` compliance ABI plus `buildLayoutIr` to ensure the WASM bridge is
89
+ wired correctly. If you tweak the Rust runtime, rerun `pnpm build:wasm` so the
90
+ tests pick up the updated binaries.
91
+
92
+ ---
93
+
94
+ ## Development notes
95
+
96
+ * The TypeScript surface intentionally stays tiny; we no longer export the old
97
+ decoder/resolver classes. Future code should talk to the WASM bridge instead
98
+ of re-implementing reflection logic in JS.
99
+ * Browser vs. Node detection happens in `src/wasmBridge.ts`. Node loads the
100
+ `wasm/node` build via `createRequire`, while bundlers dynamically import the
101
+ `wasm/bundler` module.
102
+ * The JSON shape returned by `reflect*` matches `abi_reflect`'s CLI output, so
103
+ parity debugging can use the `abi-reflect` CLI under `rpc/abi/abi_reflect`.
104
+ * Layout IR consumers can feed `buildLayoutIr` into caches or ship a prebuilt
105
+ snapshot alongside the WASM runtime to guard against future schema changes.
106
+
107
+ ---
108
+
109
+ Questions? Ping the thru-net ABI team (same folks maintaining
110
+ `rpc/abi/abi_reflect`). Whenever you extend the Rust reflection engine or shared IR,
111
+ regenerate the WASM artifacts and mention the change in `enums-fam-impl.md` so
112
+ tooling consumers know which version to depend on.
package/README.md ADDED
@@ -0,0 +1,342 @@
1
+ # @thru/sdk
2
+
3
+ Typed TypeScript/JavaScript client for talking to the Thru blockchain. The SDK exposes rich domain models (blocks, accounts, transactions, events, proofs) that hide the underlying protobuf transport.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @thru/sdk
9
+ ```
10
+
11
+ ### TypeScript Configuration
12
+
13
+ For optimal import resolution, use modern module resolution:
14
+
15
+ ```json
16
+ {
17
+ "compilerOptions": {
18
+ "moduleResolution": "bundler",
19
+ "module": "ESNext",
20
+ "target": "ES2020",
21
+ "isolatedModules": true
22
+ }
23
+ }
24
+ ```
25
+
26
+ If you rely on Node’s ESM support without a bundler, use `"moduleResolution": "nodenext"`.
27
+
28
+ ## Basic Usage
29
+
30
+ ```ts
31
+ import { createThruClient } from "@thru/sdk";
32
+ import {
33
+ Account,
34
+ Block,
35
+ ChainEvent,
36
+ Transaction,
37
+ TransactionStatusSnapshot,
38
+ } from "@thru/sdk";
39
+
40
+ const thru = createThruClient({
41
+ baseUrl: "https://grpc-web.alphanet.thruput.org",
42
+ });
43
+
44
+ // Fetch the latest finalized block
45
+ const height = await thru.blocks.getBlockHeight();
46
+ const latestBlock: Block = await thru.blocks.get({ slot: height.finalized });
47
+ console.log(latestBlock.header.blockHash);
48
+
49
+ // Fetch an account – returns the Account domain object
50
+ const account: Account = await thru.accounts.get("taExampleAddress...");
51
+ console.log(account.meta?.balance);
52
+
53
+ // Build, sign, submit, and track a transaction
54
+ const { rawTransaction, signature } = await thru.transactions.buildAndSign({
55
+ feePayer: {
56
+ publicKey: "taFeePayerAddress...",
57
+ privateKey: feePayerSecretKeyBytes,
58
+ },
59
+ program: programIdentifierBytes,
60
+ });
61
+ await thru.transactions.send(rawTransaction);
62
+
63
+ // Track the transaction – emits domain snapshots
64
+ for await (const update of thru.streaming.trackTransaction(signature)) {
65
+ console.log(update.status, update.executionResult?.consumedComputeUnits);
66
+ if (update.statusCode === ConsensusStatus.FINALIZED) break;
67
+ }
68
+ ```
69
+
70
+ ## Client Configuration
71
+
72
+ `createThruClient` accepts advanced transport options so you can customize networking, interceptors, and default call behaviour:
73
+
74
+ ```ts
75
+ const thru = createThruClient({
76
+ baseUrl: "https://grpc-web.alphanet.thruput.org",
77
+ transportOptions: {
78
+ useBinaryFormat: false,
79
+ defaultTimeoutMs: 10_000,
80
+ },
81
+ interceptors: [authInterceptor],
82
+ callOptions: {
83
+ timeoutMs: 5_000,
84
+ headers: [["x-team", "sdk"]],
85
+ },
86
+ });
87
+ ```
88
+
89
+ - `transportOptions` are passed to `createGrpcWebTransport`. Provide custom fetch implementations, JSON/binary options, or merge additional interceptors.
90
+ - `interceptors` let you append cross-cutting logic (auth, metrics) without re-implementing transports.
91
+ - `callOptions` act as defaults for **every** RPC. You can set timeouts, headers, or a shared `AbortSignal`, and each module call merges in per-request overrides.
92
+
93
+ ## Domain Models
94
+
95
+ The SDK revolves around immutable domain classes. They copy mutable buffers, expose clear invariants, and provide conversion helpers where needed.
96
+
97
+ | API surface | Domain class |
98
+ | --- | --- |
99
+ | Blocks | `Block`, `BlockHeader`, `BlockFooter` |
100
+ | Accounts | `Account`, `AccountMeta`, `AccountData` |
101
+ | Transactions | `Transaction`, `TransactionStatusSnapshot`, `TrackTransactionUpdate` |
102
+ | Events | `ChainEvent` |
103
+ | Proofs | `StateProof` |
104
+ | Height | `HeightSnapshot` |
105
+ | Node version | `VersionInfo` |
106
+
107
+ All classes are exported from the root package for easy access:
108
+
109
+ ```ts
110
+ import { Block, Account, ChainEvent } from "@thru/sdk";
111
+ ```
112
+
113
+ ### Primitives
114
+
115
+ `Pubkey` and `Signature` wrap the 32-byte Ed25519 public key and 64-byte signature primitives, respectively. They centralize validation, conversion, and proto interop so you can work with either Thru-formatted strings (`ta...` / `ts...`), hex, or raw bytes without sprinkling helpers throughout your app.
116
+
117
+ ```ts
118
+ import { Pubkey, Signature } from "@thru/sdk";
119
+
120
+ const payer = Pubkey.from("taDs2..."); // accepts ta string, hex, or Uint8Array
121
+ const sig = Signature.from("ts8Lk..."); // accepts ts string, hex, or Uint8Array
122
+
123
+ payer.toBytes(); // defensive copy
124
+ payer.toThruFmt(); // "ta..." string
125
+ payer.toProtoPubkey(); // thru.common.v1.Pubkey
126
+ payer.toProtoTaPubkey(); // thru.common.v1.TaPubkey
127
+
128
+ sig.toBytes();
129
+ sig.toThruFmt(); // "ts..." string
130
+ sig.toProtoSignature(); // thru.common.v1.Signature
131
+ sig.toProtoTsSignature(); // thru.common.v1.TsSignature
132
+
133
+ // Helper namespace now returns these domain objects:
134
+ const parsed = sdk.helpers.createPubkey("taFeePayerAddress...");
135
+ const signature = sdk.helpers.createSignature(sigBytes);
136
+ ```
137
+
138
+ The bound client accepts either raw bytes or the new primitives; call `.toBytes()` on `Pubkey`/`Signature` when you need to interop with legacy code.
139
+
140
+ ## View Options
141
+
142
+ When fetching resources, you can control which parts of the resource are returned using view options. This allows you to optimize network usage by only fetching the data you need.
143
+
144
+ ### AccountView
145
+
146
+ Controls which sections of account resources are returned:
147
+
148
+ ```ts
149
+ import { AccountView } from "@thru/sdk";
150
+
151
+ // Fetch only the account address (lightweight existence check)
152
+ const account = await thru.accounts.get(address, {
153
+ view: AccountView.PUBKEY_ONLY,
154
+ });
155
+
156
+ // Fetch only account metadata (balance, flags, owner, etc.)
157
+ const account = await thru.accounts.get(address, {
158
+ view: AccountView.META_ONLY,
159
+ });
160
+
161
+ // Fetch only account data bytes (program data)
162
+ const account = await thru.accounts.get(address, {
163
+ view: AccountView.DATA_ONLY,
164
+ });
165
+
166
+ // Fetch everything: address, metadata, and data (default)
167
+ const account = await thru.accounts.get(address, {
168
+ view: AccountView.FULL,
169
+ });
170
+ ```
171
+
172
+ | View Option | Returns | Use Case |
173
+ | --- | --- | --- |
174
+ | `AccountView.PUBKEY_ONLY` | Only the account `address` | Quick existence check |
175
+ | `AccountView.META_ONLY` | `address` + `meta` (balance, flags, owner, dataSize, seq, nonce) | Display account summary without data |
176
+ | `AccountView.DATA_ONLY` | `address` + `data` (raw bytes) | Fetch program data without metadata |
177
+ | `AccountView.FULL` | `address` + `meta` + `data` | Complete account information |
178
+
179
+ ### BlockView
180
+
181
+ Controls how much of a block resource is returned:
182
+
183
+ ```ts
184
+ import { BlockView } from "@thru/sdk";
185
+
186
+ // Fetch only block header (slot, hash, producer, etc.)
187
+ const block = await thru.blocks.get({ slot }, {
188
+ view: BlockView.HEADER_ONLY,
189
+ });
190
+
191
+ // Fetch header and footer (execution status)
192
+ const block = await thru.blocks.get({ slot }, {
193
+ view: BlockView.HEADER_AND_FOOTER,
194
+ });
195
+
196
+ // Fetch only block body (transactions)
197
+ const block = await thru.blocks.get({ slot }, {
198
+ view: BlockView.BODY_ONLY,
199
+ });
200
+
201
+ // Fetch everything: header, body, and footer (default)
202
+ const block = await thru.blocks.get({ slot }, {
203
+ view: BlockView.FULL,
204
+ });
205
+ ```
206
+
207
+ | View Option | Returns | Use Case |
208
+ | --- | --- | --- |
209
+ | `BlockView.HEADER_ONLY` | Only block `header` (metadata) | Display block summary without transactions |
210
+ | `BlockView.HEADER_AND_FOOTER` | `header` + `footer` (execution status) | Check execution status without transactions |
211
+ | `BlockView.BODY_ONLY` | Only block `body` (transactions) | Fetch transactions without header metadata |
212
+ | `BlockView.FULL` | `header` + `body` + `footer` | Complete block information |
213
+
214
+ ### TransactionView
215
+
216
+ Controls how much of a transaction resource is returned:
217
+
218
+ ```ts
219
+ import { TransactionView } from "@thru/sdk";
220
+
221
+ // Fetch only transaction signature
222
+ const tx = await thru.transactions.get(signature, {
223
+ view: TransactionView.SIGNATURE_ONLY,
224
+ });
225
+
226
+ // Fetch only transaction header (signature, fee payer, etc.)
227
+ const tx = await thru.transactions.get(signature, {
228
+ view: TransactionView.HEADER_ONLY,
229
+ });
230
+
231
+ // Fetch header and body (instructions)
232
+ const tx = await thru.transactions.get(signature, {
233
+ view: TransactionView.HEADER_AND_BODY,
234
+ });
235
+
236
+ // Fetch everything: header, body, and execution results (default)
237
+ const tx = await thru.transactions.get(signature, {
238
+ view: TransactionView.FULL,
239
+ });
240
+ ```
241
+
242
+ | View Option | Returns | Use Case |
243
+ | --- | --- | --- |
244
+ | `TransactionView.SIGNATURE_ONLY` | Only transaction `signature` | Quick existence check |
245
+ | `TransactionView.HEADER_ONLY` | Only transaction `header` (signature, fee payer, compute budget) | Display transaction summary without instructions |
246
+ | `TransactionView.HEADER_AND_BODY` | `header` + `body` (instructions) | Fetch transaction without execution results |
247
+ | `TransactionView.FULL` | `header` + `body` + execution results | Complete transaction information |
248
+
249
+ **Note:** If no view is specified, the default is `FULL` for all resource types.
250
+
251
+ ## Streaming APIs
252
+
253
+ Every streaming endpoint yields an async iterable of domain models:
254
+
255
+ ```ts
256
+ // Blocks
257
+ for await (const { block } of thru.streaming.streamBlocks()) {
258
+ console.log(block.header.slot);
259
+ }
260
+
261
+ // Account updates
262
+ for await (const { update } of thru.streaming.streamAccountUpdates("taAddress")) {
263
+ if (update.kind === "snapshot") {
264
+ console.log(update.snapshot.account.meta?.balance);
265
+ }
266
+ }
267
+
268
+ // Events
269
+ for await (const { event } of thru.streaming.streamEvents()) {
270
+ console.log((event as ChainEvent).timestampNs);
271
+ }
272
+
273
+ // Transaction tracking
274
+ for await (const update of thru.streaming.trackTransaction(signature)) {
275
+ console.log(update.status, update.executionResult?.consumedComputeUnits);
276
+ }
277
+ ```
278
+
279
+ ## Filters
280
+
281
+ Server-side filtering is supported everywhere via CEL expressions:
282
+
283
+ ```ts
284
+ import { Filter, FilterParamValue } from "@thru/sdk";
285
+
286
+ const ownerFilter = new Filter({
287
+ expression: "account.meta.owner.value == params.owner",
288
+ params: {
289
+ owner: FilterParamValue.pubkey("taExampleAddress..."),
290
+ min_balance: FilterParamValue.uint(1_000_000n),
291
+ },
292
+ });
293
+
294
+ const accounts = await thru.accounts.list({ filter: ownerFilter });
295
+ ```
296
+
297
+ Accepted parameter kinds:
298
+ - `stringValue`
299
+ - `bytesValue`
300
+ - `boolValue`
301
+ - `intValue`
302
+ - `doubleValue`
303
+ - `uintValue`
304
+ - `pubkeyValue`
305
+ - `signatureValue`
306
+ - `taPubkeyValue`
307
+ - `tsSignatureValue`
308
+
309
+ Functions that take filters:
310
+ - List APIs: `thru.accounts.list`, `thru.blocks.list`, `thru.transactions.listForAccount`
311
+ - Streams: `thru.streaming.streamBlocks`, `thru.streaming.streamAccountUpdates`, `thru.streaming.streamTransactions`, `thru.streaming.streamEvents`
312
+
313
+ Use the helper constructors on `FilterParamValue` to safely build parameters from raw bytes, ta/ts-encoded strings, or simple numbers.
314
+
315
+ ## Modules Overview
316
+
317
+ - `thru.blocks` — fetch/stream blocks and height snapshots
318
+ - `thru.accounts` — read account state or build create-account transactions
319
+ - `thru.transactions` — build, sign, submit, track, and inspect transactions
320
+ - `thru.events` — query event history
321
+ - `thru.proofs` — generate state proofs
322
+ - `thru.consensus` — build version contexts and stringify consensus states
323
+ - `thru.streaming` — streaming wrappers for blocks, accounts, transactions, events
324
+ - `thru.helpers` — address, signature, and block-hash conversion helpers
325
+
326
+ The public surface is fully domain-based; reaching for lower-level protobuf structures is no longer necessary.
327
+
328
+ ## Streaming helpers
329
+
330
+ Async iterable utilities make it easier to consume streaming APIs:
331
+
332
+ ```ts
333
+ import { collectStream, firstStreamValue } from "@thru/sdk";
334
+
335
+ const updates = await collectStream(thru.streaming.streamBlocks({ startSlot: height.finalized }), {
336
+ limit: 5,
337
+ });
338
+
339
+ const firstEvent = await firstStreamValue(thru.streaming.streamEvents());
340
+ ```
341
+
342
+ `collectStream` gathers values (optionally respecting `AbortSignal`s), `firstStreamValue` returns the first item, and `forEachStreamValue` lets you run async handlers for each streamed update.