@databricks/zerobus-ingest-sdk 1.1.0 → 1.2.0

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
@@ -42,21 +42,14 @@ The Databricks Zerobus Ingest SDK for TypeScript provides a high-performance cli
42
42
  - **Node.js**: >= 16
43
43
  - **Databricks workspace** with Zerobus access enabled
44
44
 
45
- ### Build Requirements
45
+ ### Source Build Requirements
46
46
 
47
47
  - **Rust toolchain**: 1.70 or higher - [Install Rust](https://rustup.rs/)
48
48
  - **Cargo**: Included with Rust
49
+ - Platform C/C++ build tools
49
50
 
50
- ### Dependencies
51
-
52
- These will be installed automatically:
53
-
54
- ```json
55
- {
56
- "@napi-rs/cli": "^2.18.4",
57
- "napi-build": "^0.3.3"
58
- }
59
- ```
51
+ You only need these source-build tools when npm cannot use a pre-built native
52
+ package for your platform, or when developing the SDK from this repository.
60
53
 
61
54
  ## Quick Start User Guide
62
55
 
@@ -66,108 +59,30 @@ Before using the SDK, you need a Databricks workspace URL, a Delta table, and a
66
59
 
67
60
  ### Installation
68
61
 
69
- #### Prerequisites
70
-
71
- Before installing the SDK, ensure you have the required tools:
72
-
73
- **1. Node.js >= 16**
74
-
75
- Check if Node.js is installed:
76
62
  ```bash
77
- node --version
63
+ npm install @databricks/zerobus-ingest-sdk
78
64
  ```
79
65
 
80
- If not installed, download from [nodejs.org](https://nodejs.org/).
81
-
82
- **2. Rust Toolchain (1.70+)**
66
+ On supported platforms, npm installs the TypeScript package and the matching
67
+ pre-built native binary package automatically.
83
68
 
84
- The SDK requires Rust to compile the native addon. Install using `rustup` (the official Rust installer):
69
+ #### Local Development From Source
85
70
 
86
- **On Linux and macOS:**
87
- ```bash
88
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
89
- ```
90
-
91
- Follow the prompts (typically just press Enter to accept defaults).
92
-
93
- **On Windows:**
94
-
95
- Download and run the installer from [rustup.rs](https://rustup.rs/), or use:
96
- ```powershell
97
- # Using winget
98
- winget install Rustlang.Rustup
99
-
100
- # Or download from https://rustup.rs/
101
- ```
102
-
103
- **Verify Installation:**
104
- ```bash
105
- rustc --version
106
- cargo --version
107
- ```
71
+ Clone and build from source only when modifying this SDK or when your platform
72
+ does not have a pre-built native binary:
108
73
 
109
- You should see version 1.70 or higher. If the commands aren't found, restart your terminal or add Rust to your PATH:
110
74
  ```bash
111
- # Linux/macOS
112
- source $HOME/.cargo/env
113
-
114
- # Windows (PowerShell)
115
- # Restart your terminal
75
+ git clone https://github.com/databricks/zerobus-sdk.git
76
+ cd zerobus-sdk/typescript
77
+ npm install
78
+ npm run build
116
79
  ```
117
80
 
118
- **Additional Platform Requirements:**
119
-
120
- - **Linux**: Build essentials
121
- ```bash
122
- # Ubuntu/Debian
123
- sudo apt-get install build-essential
124
-
125
- # CentOS/RHEL
126
- sudo yum groupinstall "Development Tools"
127
- ```
128
-
129
- - **macOS**: Xcode Command Line Tools
130
- ```bash
131
- xcode-select --install
132
- ```
133
-
134
- - **Windows**: Visual Studio Build Tools
135
- - Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022)
136
- - During installation, select "Desktop development with C++"
137
-
138
- #### Installation Steps
139
-
140
- 1. Clone the repository:
141
- ```bash
142
- git clone https://github.com/databricks/zerobus-sdk.git
143
- cd zerobus-sdk/ts
144
- ```
145
-
146
- 2. Install dependencies:
147
- ```bash
148
- npm install
149
- ```
150
-
151
- 3. Build the native addon:
152
- ```bash
153
- npm run build
154
- ```
155
-
156
- This will compile the Rust code into a native Node.js addon (`.node` file) for your platform.
157
-
158
- 4. Verify the build:
159
- ```bash
160
- # You should see a .node file
161
- ls -la *.node
162
- ```
163
-
164
- 5. The SDK is now ready to use! You can:
165
- - Use it directly in this directory for examples
166
- - Link it globally: `npm link`
167
- - Or copy it into your project's `node_modules`
168
-
169
81
  **Troubleshooting:**
170
82
 
83
+ - **Unsupported platform or source build requested**: Install Rust 1.70+,
84
+ Cargo, and your platform C/C++ build tools, clone this repository, and run
85
+ `npm install` followed by `npm run build` from `zerobus-sdk/typescript`
171
86
  - **"rustc: command not found"**: Restart your terminal after installing Rust
172
87
  - **Build fails on Windows**: Ensure Visual Studio Build Tools are installed with C++ support
173
88
  - **Build fails on Linux**: Install build-essential or equivalent package
@@ -182,6 +97,10 @@ The SDK supports two serialization formats. **Protocol Buffers is the default**
182
97
 
183
98
  > **Note:** If you don't specify `recordType`, the SDK will use Protocol Buffers by default. To use JSON, explicitly set `recordType: RecordType.Json`.
184
99
 
100
+ ### Acknowledgments and throughput
101
+
102
+ Ingestion is asynchronous. `ingestRecordOffset()` (and `ingestRecordsOffset()`) resolves as soon as the record is queued; the SDK sends it and tracks its acknowledgment in the background. To confirm records are durably committed, call `flush()` — it resolves once everything queued so far is acknowledged. The idiomatic flow is **ingest in a loop, then `flush()`** (once for a bounded batch, or periodically for a long-running stream). Each ingest also returns the record's offset, and `waitForOffset(offset)` resolves when that offset is acknowledged — handy when a specific record must be confirmed before continuing (acks are ordered, so waiting on the last offset confirms the whole run). Just avoid calling `waitForOffset()` after every record in a tight loop, since that limits throughput to one record per round-trip. The examples below follow this pattern.
103
+
185
104
  ### Option 1: Using JSON (Quick Start)
186
105
 
187
106
  JSON mode is the simplest way to get started. You don't need to define or compile protobuf schemas, but you must explicitly specify `RecordType.Json`.
@@ -189,12 +108,13 @@ JSON mode is the simplest way to get started. You don't need to define or compil
189
108
  ```typescript
190
109
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
191
110
 
111
+ async function main(): Promise<void> {
192
112
  // Configuration
193
113
  // For AWS:
194
114
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
195
115
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
196
116
  // For Azure:
197
- // const zerobusEndpoint = '<workspace-id>.zerobus.<region>.azuredatabricks.net';
117
+ // const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.azuredatabricks.net';
198
118
  // const workspaceUrl = 'https://<workspace-name>.azuredatabricks.net';
199
119
 
200
120
  const tableName = 'main.default.air_quality';
@@ -223,8 +143,6 @@ const stream = await sdk.createStream(
223
143
  );
224
144
 
225
145
  try {
226
- let lastOffset: bigint;
227
-
228
146
  // Send all records
229
147
  for (let i = 0; i < 100; i++) {
230
148
  const record = {
@@ -233,16 +151,23 @@ try {
233
151
  humidity: 50 + (i % 40)
234
152
  };
235
153
 
236
- // ingestRecordOffset returns immediately after queuing
237
- lastOffset = await stream.ingestRecordOffset(record);
154
+ // Queue the record; do not wait for its acknowledgement here
155
+ await stream.ingestRecordOffset(record);
238
156
  }
239
157
 
240
158
  // Wait for all records to be acknowledged
241
- await stream.waitForOffset(lastOffset);
159
+ await stream.flush();
242
160
  console.log('Successfully ingested 100 records!');
243
161
  } finally {
244
162
  await stream.close();
245
163
  }
164
+
165
+ }
166
+
167
+ main().catch((error) => {
168
+ console.error('Fatal error:', error);
169
+ process.exitCode = 1;
170
+ });
246
171
  ```
247
172
 
248
173
  ### Option 2: Using Protocol Buffers (Default, Recommended)
@@ -254,7 +179,15 @@ Protocol Buffers is the default serialization format and provides efficient bina
254
179
  Before starting, ensure you have:
255
180
 
256
181
  1. **Protocol Buffer Compiler (`protoc`)** - Required for generating descriptor files
257
- 2. **protobufjs** and **protobufjs-cli** - Already included in package.json devDependencies
182
+ 2. **protobufjs** - Required at runtime by your generated Protocol Buffer code
183
+ 3. **protobufjs-cli** - Required during development to generate JavaScript and type declarations
184
+
185
+ Install the JavaScript runtime and code-generation tools in your application:
186
+
187
+ ```bash
188
+ npm install protobufjs
189
+ npm install --save-dev protobufjs-cli
190
+ ```
258
191
 
259
192
  #### Step 1: Install Protocol Buffer Compiler
260
193
 
@@ -295,7 +228,9 @@ protoc --version
295
228
 
296
229
  #### Step 2: Define Your Protocol Buffer Schema
297
230
 
298
- The SDK includes an example schema at `schemas/air_quality.proto`:
231
+ Create `schemas/air_quality.proto` in your application with the following
232
+ example schema. Also create an `examples/generated` directory for the generated
233
+ JavaScript and type declarations:
299
234
 
300
235
  ```protobuf
301
236
  syntax = "proto2";
@@ -315,13 +250,8 @@ message AirQuality {
315
250
  Generate TypeScript code from your proto schema:
316
251
 
317
252
  ```bash
318
- npm run build:proto
319
- ```
320
-
321
- This runs:
322
- ```bash
323
- pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
324
- pbts -o examples/generated/air_quality.d.ts examples/generated/air_quality.js
253
+ npx pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
254
+ npx pbts -o examples/generated/air_quality.d.ts examples/generated/air_quality.js
325
255
  ```
326
256
 
327
257
  **Output:**
@@ -351,8 +281,9 @@ That's it! The SDK will automatically extract the message descriptor from this f
351
281
  ```typescript
352
282
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
353
283
  import * as airQuality from './examples/generated/air_quality';
354
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
284
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
355
285
 
286
+ async function main(): Promise<void> {
356
287
  // Configuration
357
288
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
358
289
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
@@ -388,26 +319,32 @@ const stream = await sdk.createStream(tableProperties, clientId, clientSecret, o
388
319
 
389
320
  try {
390
321
  const AirQuality = airQuality.examples.AirQuality;
391
- let lastOffset: bigint;
392
322
 
393
323
  // Send all records
394
324
  for (let i = 0; i < 100; i++) {
395
325
  const record = AirQuality.create({
396
- device_name: `sensor-${i}`,
326
+ deviceName: `sensor-${i}`,
397
327
  temp: 20 + i,
398
328
  humidity: 50 + i
399
329
  });
400
330
 
401
- // ingestRecordOffset returns immediately after queuing
402
- lastOffset = await stream.ingestRecordOffset(record);
331
+ // Queue the record; do not wait for its acknowledgement here
332
+ await stream.ingestRecordOffset(record);
403
333
  }
404
334
 
405
335
  // Wait for all records to be acknowledged
406
- await stream.waitForOffset(lastOffset);
336
+ await stream.flush();
407
337
  console.log('Successfully ingested 100 records!');
408
338
  } finally {
409
339
  await stream.close();
410
340
  }
341
+
342
+ }
343
+
344
+ main().catch((error) => {
345
+ console.error('Fatal error:', error);
346
+ process.exitCode = 1;
347
+ });
411
348
  ```
412
349
 
413
350
  #### Type Mapping: Delta ↔ Protocol Buffers
@@ -466,24 +403,16 @@ message NestedData {
466
403
  EOF
467
404
  ```
468
405
 
469
- 2. **Add build script to package.json:**
470
- ```json
471
- {
472
- "scripts": {
473
- "build:proto:myschema": "pbjs -t static-module -w commonjs -o examples/generated/my_schema.js schemas/my_schema.proto && pbts -o examples/generated/my_schema.d.ts examples/generated/my_schema.js"
474
- }
475
- }
476
- ```
477
-
478
- 3. **Generate code and descriptor:**
406
+ 2. **Generate code and descriptor:**
479
407
  ```bash
480
- npm run build:proto:myschema
408
+ npx pbjs -t static-module -w commonjs -o examples/generated/my_schema.js schemas/my_schema.proto
409
+ npx pbts -o examples/generated/my_schema.d.ts examples/generated/my_schema.js
481
410
  protoc --descriptor_set_out=schemas/my_schema_descriptor.pb --include_imports schemas/my_schema.proto
482
411
  ```
483
412
 
484
- 4. **Load descriptor in your code:**
413
+ 3. **Load descriptor in your code:**
485
414
  ```typescript
486
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
415
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
487
416
  const descriptorBase64 = loadDescriptorProto({
488
417
  descriptorPath: 'schemas/my_schema_descriptor.pb',
489
418
  protoFileName: 'my_schema.proto',
@@ -497,7 +426,7 @@ message NestedData {
497
426
  - Install `protoc` (see Step 1 above)
498
427
 
499
428
  **"Cannot find module './generated/air_quality'"**
500
- - Run `npm run build:proto` to generate TypeScript code
429
+ - Run the `npx pbjs` and `npx pbts` commands from Step 3
501
430
 
502
431
  **"Descriptor file not found"**
503
432
  - Generate the descriptor file using the commands in Step 4
@@ -509,27 +438,27 @@ message NestedData {
509
438
  - Make sure you're using `loadDescriptorProto()` from the utils
510
439
 
511
440
  **Build fails on proto generation**
512
- - Ensure protobufjs is installed: `npm install --save-dev protobufjs protobufjs-cli`
441
+ - Ensure the runtime and CLI are installed: `npm install protobufjs` and
442
+ `npm install --save-dev protobufjs-cli`
513
443
 
514
444
  #### Quick Reference
515
445
 
516
- Complete setup from scratch:
446
+ After creating `schemas/air_quality.proto` and the `examples/generated`
447
+ directory as described above:
517
448
  ```bash
518
- # Install dependencies and build SDK
519
- npm install
520
- npm run build
449
+ # Install the SDK and protobuf codegen tools
450
+ npm install @databricks/zerobus-ingest-sdk protobufjs
451
+ npm install --save-dev protobufjs-cli
521
452
 
522
- # Setup Protocol Buffers
523
- npm run build:proto
453
+ # Generate protobuf code and descriptor
454
+ npx pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
455
+ npx pbts -o examples/generated/air_quality.d.ts examples/generated/air_quality.js
524
456
  protoc --descriptor_set_out=schemas/air_quality_descriptor.pb --include_imports schemas/air_quality.proto
525
-
526
- # Run example
527
- npm run example:proto:single
528
457
  ```
529
458
 
530
459
  #### Why Two Steps (TypeScript + Descriptor)?
531
460
 
532
- 1. **TypeScript Code Generation** (`npm run build:proto`):
461
+ 1. **TypeScript Code Generation** (`npx pbjs` and `npx pbts`):
533
462
  - Creates JavaScript/TypeScript code for your application
534
463
  - Provides type-safe message creation and encoding
535
464
  - Used in your application code
@@ -543,10 +472,14 @@ Both are necessary for Protocol Buffers ingestion!
543
472
 
544
473
  ## Usage Examples
545
474
 
546
- See the `examples/` directory for complete, runnable examples. See [examples/README.md](examples/README.md) for detailed instructions.
475
+ The source repository contains complete, runnable examples in `examples/`.
476
+ Clone and build the repository using the [local development](#local-development-from-source)
477
+ instructions, then see [examples/README.md](examples/README.md) for details.
547
478
 
548
479
  ### Running Examples
549
480
 
481
+ Run these commands from the cloned repository's `typescript` directory:
482
+
550
483
  ```bash
551
484
  # Set environment variables
552
485
  export ZEROBUS_SERVER_ENDPOINT="https://<workspace-id>.zerobus.<region>.cloud.databricks.com"
@@ -576,7 +509,7 @@ For higher throughput, use batch ingestion to send multiple records with a singl
576
509
 
577
510
  ```typescript
578
511
  const records = Array.from({ length: 1000 }, (_, i) =>
579
- AirQuality.create({ device_name: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
512
+ AirQuality.create({ deviceName: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
580
513
  );
581
514
 
582
515
  // Protobuf Type 1: Message objects (high-level) - SDK auto-serializes
@@ -659,7 +592,7 @@ const stream = await sdk.createStream(
659
592
  '', // client_secret (ignored when headers_provider is provided)
660
593
  options,
661
594
  {
662
- getHeadersCallback: async () => [
595
+ getHeadersCallback: () => [
663
596
  ["authorization", `Bearer ${myToken}`],
664
597
  ["x-databricks-zerobus-table-name", tableName]
665
598
  ]
@@ -680,7 +613,7 @@ const stream = await sdk.createStream(
680
613
  | Option | Default | Description |
681
614
  |--------|---------|-------------|
682
615
  | `recordType` | `RecordType.Proto` | Serialization format: `RecordType.Json` or `RecordType.Proto` |
683
- | `maxInflightRequests` | 10,000 | Maximum number of unacknowledged requests |
616
+ | `maxInflightRequests` | 1,000,000 | Maximum number of unacknowledged requests |
684
617
  | `recovery` | true | Enable automatic stream recovery |
685
618
  | `recoveryTimeoutMs` | 15,000 | Timeout for recovery operations (ms) |
686
619
  | `recoveryBackoffMs` | 2,000 | Delay between recovery attempts (ms) |
@@ -714,13 +647,15 @@ const stream = await sdk.createStream(
714
647
  ## Descriptor Utilities
715
648
 
716
649
  The SDK provides a helper function to extract Protocol Buffer descriptors from FileDescriptorSets.
650
+ Use the `.js` subpath shown below for compatibility with CommonJS and native
651
+ Node.js ESM imports.
717
652
 
718
653
  ### loadDescriptorProto()
719
654
 
720
655
  Extracts a specific message descriptor from a FileDescriptorSet:
721
656
 
722
657
  ```typescript
723
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
658
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
724
659
 
725
660
  const descriptorBase64 = loadDescriptorProto({
726
661
  descriptorPath: 'schemas/my_schema_descriptor.pb',
@@ -753,38 +688,37 @@ const descriptorBase64 = loadDescriptorProto({
753
688
 
754
689
  ## Error Handling
755
690
 
756
- The SDK includes automatic recovery for transient failures (enabled by default with `recovery: true`). For permanent failures, use `recreateStream()` to automatically recover all unacknowledged batches. Always use try/finally blocks to ensure streams are properly closed:
691
+ The SDK includes automatic recovery for transient failures (enabled by default with `recovery: true`). `getUnackedBatches()` and `recreateStream()` succeed only after a terminal native-stream failure, which already closes the stream. An enqueue failure leaves the wrapper active, so those calls reject; rethrow the original error. Do not call `stream.close()` before `recreateStream()`, because close releases the native handle.
757
692
 
758
693
  ```typescript
694
+ let replacement;
759
695
  try {
760
696
  const offset = await stream.ingestRecordOffset(record);
761
- await stream.waitForOffset(offset);
697
+ await stream.flush();
762
698
  console.log(`Success: offset ${offset}`);
763
699
  } catch (error) {
764
700
  console.error('Ingestion failed:', error);
765
-
766
- // When stream fails, close it first
767
- await stream.close();
768
- console.log('Stream closed after error');
769
-
770
- // Optional: Inspect what needs recovery (must be called on closed stream)
771
- const unackedBatches = await stream.getUnackedBatches();
772
- console.log(`Batches to recover: ${unackedBatches.length}`);
773
-
774
- // Recommended recovery approach: Use recreateStream()
775
- // This method:
776
- // 1. Gets all unacknowledged batches from the failed stream
777
- // 2. Creates a new stream with the same configuration
778
- // 3. Re-ingests all unacknowledged batches automatically
779
- // 4. Returns the new stream ready for continued use
780
- const newStream = await sdk.recreateStream(stream);
781
- console.log(`Stream recreated with ${unackedBatches.length} batches re-ingested`);
782
-
783
- // Continue using newStream for further ingestion
784
701
  try {
785
- // Continue ingesting...
702
+ const unackedBatches = await stream.getUnackedBatches();
703
+ console.log(`Batches to recover: ${unackedBatches.length}`);
704
+ replacement = await sdk.recreateStream(stream);
705
+ await replacement.flush();
706
+ } catch (recoveryError) {
707
+ console.error('Stream was not terminal or recovery failed:', recoveryError);
708
+ throw new AggregateError(
709
+ [error, recoveryError],
710
+ 'ingestion and recovery both failed',
711
+ );
786
712
  } finally {
787
- await newStream.close();
713
+ if (replacement) {
714
+ await replacement.close();
715
+ }
716
+ }
717
+ } finally {
718
+ try {
719
+ await stream.close();
720
+ } catch (closeError) {
721
+ console.error('Failed stream released:', closeError);
788
722
  }
789
723
  }
790
724
  ```
@@ -804,12 +738,14 @@ Main entry point for the SDK.
804
738
  **Constructor:**
805
739
 
806
740
  ```typescript
807
- new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string)
741
+ new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string, options?: ZerobusSdkOptions)
808
742
  ```
809
743
 
810
744
  **Parameters:**
811
745
  - `zerobusEndpoint` (string) - The Zerobus gRPC endpoint (e.g., `https://<workspace-id>.zerobus.<region>.cloud.databricks.com` for AWS, or `https://<workspace-id>.zerobus.<region>.azuredatabricks.net` for Azure)
812
746
  - `unityCatalogUrl` (string) - The Unity Catalog endpoint (your workspace URL)
747
+ - `options` (ZerobusSdkOptions, optional) - Additional SDK configuration:
748
+ - `applicationName` (string, optional) - Application identifier appended to the HTTP `user-agent` header, conventionally `"<product>/<version>"` (e.g. `"my-app/1.0"`). The header becomes `zerobus-sdk-ts/<version> <applicationName>`, enabling server-side attribution.
813
749
 
814
750
  **Methods:**
815
751
 
@@ -845,23 +781,29 @@ This method is the **recommended approach** for recovering from stream failures.
845
781
  4. Returns the new stream ready for continued ingestion
846
782
 
847
783
  **Parameters:**
848
- - `stream` - The failed or closed stream to recreate
784
+ - `stream` - The terminally failed stream to recreate. Do not call `stream.close()`
785
+ first because the TypeScript wrapper releases its native handle on close.
849
786
 
850
787
  **Returns:** Promise resolving to a new `ZerobusStream` with all unacknowledged batches re-ingested
851
788
 
852
789
  **Example:**
853
790
  ```typescript
854
791
  try {
855
- await stream.ingestRecords(batch);
792
+ await stream.ingestRecordsOffset(batch);
793
+ await stream.flush();
856
794
  } catch (error) {
857
- await stream.close();
858
- // Automatically recreate stream and recover all unacked batches
795
+ // recreateStream() rejects unless the native stream already failed closed.
859
796
  const newStream = await sdk.recreateStream(stream);
860
- // Continue ingesting with newStream
797
+ try {
798
+ await newStream.flush();
799
+ } finally {
800
+ await newStream.close();
801
+ }
861
802
  }
862
803
  ```
863
804
 
864
- **Note:** This method preserves batch structure and re-ingests batches atomically. For debugging, you can inspect what was recovered using `getUnackedBatches()` after closing the stream.
805
+ **Note:** This method preserves batch structure and re-ingests batches atomically. For
806
+ debugging, inspect `getUnackedBatches()` after a terminal failure and before closing the wrapper.
865
807
 
866
808
  ---
867
809
 
@@ -875,13 +817,16 @@ Represents an active ingestion stream.
875
817
  async ingestRecordOffset(payload: Buffer | string | object): Promise<bigint>
876
818
  ```
877
819
 
878
- **(Recommended)** Ingests a single record. The Promise resolves immediately after the record is queued (before server acknowledgment). Use `waitForOffset()` to wait for acknowledgment when needed.
820
+ **(Recommended)** Ingests a single record. The Promise resolves immediately after the record is queued (before server acknowledgment); the round-trip happens in the background. The idiomatic flow is to ingest in a loop and then `flush()` once to confirm everything queued so far. The returned offset, together with `waitForOffset()`, lets you confirm a specific record when needed — prefer that for bulk over waiting after each record, since per-record waiting limits throughput to one round-trip per record.
879
821
 
880
822
  ```typescript
881
- // High-throughput pattern: send many, wait once
882
- const offset1 = await stream.ingestRecordOffset(record1); // Resolves immediately
883
- const offset2 = await stream.ingestRecordOffset(record2); // Resolves immediately
884
- await stream.waitForOffset(offset2); // Waits for server to acknowledge all records up to offset2
823
+ // Idiomatic flow: ingest in a loop, then flush once
824
+ let lastOffset: bigint | null = null;
825
+ for (const record of records) {
826
+ lastOffset = await stream.ingestRecordOffset(record); // Resolves immediately
827
+ }
828
+ await stream.flush(); // Resolves once everything queued so far is acknowledged
829
+ // (Or, to confirm a specific record: if (lastOffset !== null) await stream.waitForOffset(lastOffset))
885
830
  ```
886
831
 
887
832
  ---
@@ -890,7 +835,7 @@ await stream.waitForOffset(offset2); // Waits for server to acknowledge all rec
890
835
  async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bigint | null>
891
836
  ```
892
837
 
893
- **(Recommended)** Ingests multiple records as a batch. The Promise resolves immediately after the batch is queued (before server acknowledgment). Returns `null` for empty batches.
838
+ **(Recommended)** Ingests multiple records as a batch. The Promise resolves immediately after the batch is queued (before server acknowledgment); the round-trip happens in the background. Returns `null` for empty batches. As with `ingestRecordOffset()`, the idiomatic flow is to ingest in a loop and `flush()` once to confirm; reach for `waitForOffset()` when a specific batch must be confirmed before continuing.
894
839
 
895
840
  ---
896
841
 
@@ -898,7 +843,7 @@ async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bi
898
843
  async waitForOffset(offsetId: bigint): Promise<void>
899
844
  ```
900
845
 
901
- Waits for the server to acknowledge all records up to and including the specified offset ID.
846
+ Waits for the server to acknowledge all records up to and including the specified offset ID. Acks are ordered, so waiting on the **last** offset confirms every prior record too. Use this when a specific record must be confirmed before continuing; for confirming a bulk run, `flush()` is usually simpler. Avoid calling it after every record in a tight loop, since that limits throughput to one record per round-trip.
902
847
 
903
848
  ---
904
849
 
@@ -998,7 +943,7 @@ await stream.ingestRecords(buffers);
998
943
  async flush(): Promise<void>
999
944
  ```
1000
945
 
1001
- Flushes all pending records and waits for acknowledgments.
946
+ Flushes all pending records and waits for acknowledgments. This is the recommended way to confirm a batch of `ingestRecordOffset()` / `ingestRecordsOffset()` calls: ingest in a loop without waiting, then `flush()` once at the end instead of calling `waitForOffset()` after every record.
1002
947
 
1003
948
  ```typescript
1004
949
  async close(): Promise<void>
@@ -1012,7 +957,8 @@ async getUnackedRecords(): Promise<Buffer[]>
1012
957
 
1013
958
  Returns unacknowledged record payloads as a flat array for inspection purposes.
1014
959
 
1015
- **Important:** Can only be called on **closed streams**. Call `stream.close()` first, or this will throw an error.
960
+ **Important:** This can only be called after a terminal stream failure. Do not call
961
+ `stream.close()` first: the TypeScript wrapper releases the underlying stream handle on close.
1016
962
 
1017
963
  **Returns:** Array of Buffer containing the raw record payloads
1018
964
 
@@ -1026,7 +972,8 @@ async getUnackedBatches(): Promise<Buffer[][]>
1026
972
 
1027
973
  Returns unacknowledged records grouped by their original batches for inspection purposes.
1028
974
 
1029
- **Important:** Can only be called on **closed streams**. Call `stream.close()` first, or this will throw an error.
975
+ **Important:** This can only be called after a terminal stream failure. Do not call
976
+ `stream.close()` first: the TypeScript wrapper releases the underlying stream handle on close.
1030
977
 
1031
978
  **Returns:** Array of arrays, where each inner array represents a batch of records as Buffers
1032
979
 
@@ -1039,15 +986,11 @@ try {
1039
986
  await stream.ingestRecords(batch2);
1040
987
  // ... error occurs
1041
988
  } catch (error) {
1042
- await stream.close();
1043
989
  const unackedBatches = await stream.getUnackedBatches();
1044
990
  // unackedBatches[0] contains records from batch1 (if not acked)
1045
991
  // unackedBatches[1] contains records from batch2 (if not acked)
1046
992
 
1047
- // Re-ingest with new stream
1048
- for (const batch of unackedBatches) {
1049
- await newStream.ingestRecords(batch);
1050
- }
993
+ console.log(`Batches available for recovery: ${unackedBatches.length}`);
1051
994
  }
1052
995
  ```
1053
996
 
@@ -1070,10 +1013,10 @@ interface TableProperties {
1070
1013
 
1071
1014
  ```typescript
1072
1015
  // JSON mode
1073
- const tableProperties = { tableName: 'main.default.air_quality' };
1016
+ const jsonTableProperties = { tableName: 'main.default.air_quality' };
1074
1017
 
1075
1018
  // Protocol Buffers mode
1076
- const tableProperties = {
1019
+ const protoTableProperties = {
1077
1020
  tableName: 'main.default.air_quality',
1078
1021
  descriptorProto: descriptorBase64 // Required for protobuf
1079
1022
  };
@@ -1090,7 +1033,7 @@ Configuration options for stream behavior.
1090
1033
  ```typescript
1091
1034
  interface StreamConfigurationOptions {
1092
1035
  recordType?: RecordType; // RecordType.Json or RecordType.Proto. Default: RecordType.Proto
1093
- maxInflightRequests?: number; // Default: 10,000
1036
+ maxInflightRequests?: number; // Default: 1,000,000
1094
1037
  recovery?: boolean; // Default: true
1095
1038
  recoveryTimeoutMs?: number; // Default: 15,000
1096
1039
  recoveryBackoffMs?: number; // Default: 2,000
@@ -1110,11 +1053,12 @@ enum RecordType {
1110
1053
 
1111
1054
  1. **Reuse SDK instances**: Create one `ZerobusSdk` instance per application
1112
1055
  2. **Stream lifecycle**: Always close streams in a `finally` block to ensure all records are flushed
1113
- 3. **Batch size**: Adjust `maxInflightRequests` based on your throughput requirements (default: 10,000)
1056
+ 3. **Batch size**: Adjust `maxInflightRequests` based on your throughput requirements (default: 1,000,000)
1114
1057
  4. **Error handling**: The stream handles errors internally with automatic retry. Only use `recreateStream()` for persistent failures after internal retries are exhausted.
1115
1058
  5. **Use Protocol Buffers for production**: Protocol Buffers (the default) provides better performance and schema validation. Use JSON only when you need schema flexibility or for quick prototyping.
1116
1059
  6. **Store credentials securely**: Use environment variables, never hardcode credentials
1117
1060
  7. **Use batch ingestion**: For high-throughput scenarios, use `ingestRecordsOffset()` instead of individual `ingestRecordOffset()` calls
1061
+ 8. **Ingest in a loop, then `flush()`**: See [Acknowledgments and throughput](#acknowledgments-and-throughput) above for the full explanation.
1118
1062
 
1119
1063
  ## Platform Support
1120
1064
 
@@ -1161,7 +1105,7 @@ This SDK wraps the high-performance [Rust Zerobus SDK](https://github.com/databr
1161
1105
  **Benefits:**
1162
1106
  - **Native performance** - Rust implementation for high-throughput ingestion
1163
1107
  - **Native async/await support** - Rust futures become JavaScript Promises
1164
- - **Automatic memory management** - No manual cleanup required
1108
+ - **Automatic memory management** for native objects. You still must `await stream.close()` to flush and release the stream.
1165
1109
  - **Type safety** - Compile-time checks on both sides
1166
1110
 
1167
1111
  ## Community and Contributing