@databricks/zerobus-ingest-sdk 1.1.0 → 1.3.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
@@ -28,7 +28,7 @@ The Databricks Zerobus Ingest SDK for TypeScript provides a high-performance cli
28
28
  - **High-throughput ingestion**: Optimized for high-volume data ingestion with native Rust implementation
29
29
  - **Automatic recovery**: Built-in retry and recovery mechanisms for transient failures
30
30
  - **Flexible configuration**: Customizable stream behavior and timeouts
31
- - **Multiple serialization formats**: Support for JSON, Protocol Buffers, and Arrow Flight (Beta) with optional LZ4 / ZSTD compression
31
+ - **Multiple serialization formats**: Support for JSON, Protocol Buffers, and Arrow Flight ingestion with optional LZ4 / ZSTD compression
32
32
  - **Type widening**: Accept high-level types (plain objects, protobuf messages) or low-level types (strings, buffers) - automatically handles serialization
33
33
  - **Batch ingestion**: Ingest multiple records with a single acknowledgment for higher throughput
34
34
  - **OAuth 2.0 authentication**: Secure authentication with client credentials
@@ -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
@@ -175,13 +90,18 @@ source $HOME/.cargo/env
175
90
 
176
91
  ### Choose Your Serialization Format
177
92
 
178
- The SDK supports two serialization formats. **Protocol Buffers is the default** and recommended for production use:
93
+ The SDK supports three ingestion formats. Protocol Buffers is the default record format and recommended for production row-oriented workloads:
179
94
 
180
95
  - **Protocol Buffers (Default)** - Strongly-typed schemas, efficient binary encoding, better performance. This is the default format.
181
96
  - **JSON** - Simple, no schema compilation needed. Good for getting started quickly or when schema flexibility is needed.
97
+ - Arrow Flight - High-performance columnar ingestion for applications that already produce Arrow data. See the [Arrow example](examples/arrow/README.md).
182
98
 
183
99
  > **Note:** If you don't specify `recordType`, the SDK will use Protocol Buffers by default. To use JSON, explicitly set `recordType: RecordType.Json`.
184
100
 
101
+ ### Acknowledgments and throughput
102
+
103
+ 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.
104
+
185
105
  ### Option 1: Using JSON (Quick Start)
186
106
 
187
107
  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 +109,13 @@ JSON mode is the simplest way to get started. You don't need to define or compil
189
109
  ```typescript
190
110
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
191
111
 
112
+ async function main(): Promise<void> {
192
113
  // Configuration
193
114
  // For AWS:
194
115
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
195
116
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
196
117
  // For Azure:
197
- // const zerobusEndpoint = '<workspace-id>.zerobus.<region>.azuredatabricks.net';
118
+ // const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.azuredatabricks.net';
198
119
  // const workspaceUrl = 'https://<workspace-name>.azuredatabricks.net';
199
120
 
200
121
  const tableName = 'main.default.air_quality';
@@ -223,8 +144,6 @@ const stream = await sdk.createStream(
223
144
  );
224
145
 
225
146
  try {
226
- let lastOffset: bigint;
227
-
228
147
  // Send all records
229
148
  for (let i = 0; i < 100; i++) {
230
149
  const record = {
@@ -233,16 +152,23 @@ try {
233
152
  humidity: 50 + (i % 40)
234
153
  };
235
154
 
236
- // ingestRecordOffset returns immediately after queuing
237
- lastOffset = await stream.ingestRecordOffset(record);
155
+ // Queue the record; do not wait for its acknowledgement here
156
+ await stream.ingestRecordOffset(record);
238
157
  }
239
158
 
240
159
  // Wait for all records to be acknowledged
241
- await stream.waitForOffset(lastOffset);
160
+ await stream.flush();
242
161
  console.log('Successfully ingested 100 records!');
243
162
  } finally {
244
163
  await stream.close();
245
164
  }
165
+
166
+ }
167
+
168
+ main().catch((error) => {
169
+ console.error('Fatal error:', error);
170
+ process.exitCode = 1;
171
+ });
246
172
  ```
247
173
 
248
174
  ### Option 2: Using Protocol Buffers (Default, Recommended)
@@ -254,7 +180,15 @@ Protocol Buffers is the default serialization format and provides efficient bina
254
180
  Before starting, ensure you have:
255
181
 
256
182
  1. **Protocol Buffer Compiler (`protoc`)** - Required for generating descriptor files
257
- 2. **protobufjs** and **protobufjs-cli** - Already included in package.json devDependencies
183
+ 2. **protobufjs** - Required at runtime by your generated Protocol Buffer code
184
+ 3. **protobufjs-cli** - Required during development to generate JavaScript and type declarations
185
+
186
+ Install the JavaScript runtime and code-generation tools in your application:
187
+
188
+ ```bash
189
+ npm install protobufjs
190
+ npm install --save-dev protobufjs-cli
191
+ ```
258
192
 
259
193
  #### Step 1: Install Protocol Buffer Compiler
260
194
 
@@ -295,7 +229,9 @@ protoc --version
295
229
 
296
230
  #### Step 2: Define Your Protocol Buffer Schema
297
231
 
298
- The SDK includes an example schema at `schemas/air_quality.proto`:
232
+ Create `schemas/air_quality.proto` in your application with the following
233
+ example schema. Also create an `examples/generated` directory for the generated
234
+ JavaScript and type declarations:
299
235
 
300
236
  ```protobuf
301
237
  syntax = "proto2";
@@ -315,13 +251,8 @@ message AirQuality {
315
251
  Generate TypeScript code from your proto schema:
316
252
 
317
253
  ```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
254
+ npx pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
255
+ npx pbts -o examples/generated/air_quality.d.ts examples/generated/air_quality.js
325
256
  ```
326
257
 
327
258
  **Output:**
@@ -351,8 +282,9 @@ That's it! The SDK will automatically extract the message descriptor from this f
351
282
  ```typescript
352
283
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
353
284
  import * as airQuality from './examples/generated/air_quality';
354
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
285
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
355
286
 
287
+ async function main(): Promise<void> {
356
288
  // Configuration
357
289
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
358
290
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
@@ -388,26 +320,32 @@ const stream = await sdk.createStream(tableProperties, clientId, clientSecret, o
388
320
 
389
321
  try {
390
322
  const AirQuality = airQuality.examples.AirQuality;
391
- let lastOffset: bigint;
392
323
 
393
324
  // Send all records
394
325
  for (let i = 0; i < 100; i++) {
395
326
  const record = AirQuality.create({
396
- device_name: `sensor-${i}`,
327
+ deviceName: `sensor-${i}`,
397
328
  temp: 20 + i,
398
329
  humidity: 50 + i
399
330
  });
400
331
 
401
- // ingestRecordOffset returns immediately after queuing
402
- lastOffset = await stream.ingestRecordOffset(record);
332
+ // Queue the record; do not wait for its acknowledgement here
333
+ await stream.ingestRecordOffset(record);
403
334
  }
404
335
 
405
336
  // Wait for all records to be acknowledged
406
- await stream.waitForOffset(lastOffset);
337
+ await stream.flush();
407
338
  console.log('Successfully ingested 100 records!');
408
339
  } finally {
409
340
  await stream.close();
410
341
  }
342
+
343
+ }
344
+
345
+ main().catch((error) => {
346
+ console.error('Fatal error:', error);
347
+ process.exitCode = 1;
348
+ });
411
349
  ```
412
350
 
413
351
  #### Type Mapping: Delta ↔ Protocol Buffers
@@ -466,24 +404,16 @@ message NestedData {
466
404
  EOF
467
405
  ```
468
406
 
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:**
407
+ 2. **Generate code and descriptor:**
479
408
  ```bash
480
- npm run build:proto:myschema
409
+ npx pbjs -t static-module -w commonjs -o examples/generated/my_schema.js schemas/my_schema.proto
410
+ npx pbts -o examples/generated/my_schema.d.ts examples/generated/my_schema.js
481
411
  protoc --descriptor_set_out=schemas/my_schema_descriptor.pb --include_imports schemas/my_schema.proto
482
412
  ```
483
413
 
484
- 4. **Load descriptor in your code:**
414
+ 3. **Load descriptor in your code:**
485
415
  ```typescript
486
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
416
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
487
417
  const descriptorBase64 = loadDescriptorProto({
488
418
  descriptorPath: 'schemas/my_schema_descriptor.pb',
489
419
  protoFileName: 'my_schema.proto',
@@ -497,7 +427,7 @@ message NestedData {
497
427
  - Install `protoc` (see Step 1 above)
498
428
 
499
429
  **"Cannot find module './generated/air_quality'"**
500
- - Run `npm run build:proto` to generate TypeScript code
430
+ - Run the `npx pbjs` and `npx pbts` commands from Step 3
501
431
 
502
432
  **"Descriptor file not found"**
503
433
  - Generate the descriptor file using the commands in Step 4
@@ -509,27 +439,27 @@ message NestedData {
509
439
  - Make sure you're using `loadDescriptorProto()` from the utils
510
440
 
511
441
  **Build fails on proto generation**
512
- - Ensure protobufjs is installed: `npm install --save-dev protobufjs protobufjs-cli`
442
+ - Ensure the runtime and CLI are installed: `npm install protobufjs` and
443
+ `npm install --save-dev protobufjs-cli`
513
444
 
514
445
  #### Quick Reference
515
446
 
516
- Complete setup from scratch:
447
+ After creating `schemas/air_quality.proto` and the `examples/generated`
448
+ directory as described above:
517
449
  ```bash
518
- # Install dependencies and build SDK
519
- npm install
520
- npm run build
450
+ # Install the SDK and protobuf codegen tools
451
+ npm install @databricks/zerobus-ingest-sdk protobufjs
452
+ npm install --save-dev protobufjs-cli
521
453
 
522
- # Setup Protocol Buffers
523
- npm run build:proto
454
+ # Generate protobuf code and descriptor
455
+ npx pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
456
+ npx pbts -o examples/generated/air_quality.d.ts examples/generated/air_quality.js
524
457
  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
458
  ```
529
459
 
530
460
  #### Why Two Steps (TypeScript + Descriptor)?
531
461
 
532
- 1. **TypeScript Code Generation** (`npm run build:proto`):
462
+ 1. **TypeScript Code Generation** (`npx pbjs` and `npx pbts`):
533
463
  - Creates JavaScript/TypeScript code for your application
534
464
  - Provides type-safe message creation and encoding
535
465
  - Used in your application code
@@ -543,10 +473,14 @@ Both are necessary for Protocol Buffers ingestion!
543
473
 
544
474
  ## Usage Examples
545
475
 
546
- See the `examples/` directory for complete, runnable examples. See [examples/README.md](examples/README.md) for detailed instructions.
476
+ The source repository contains complete, runnable examples in `examples/`.
477
+ Clone and build the repository using the [local development](#local-development-from-source)
478
+ instructions, then see [examples/README.md](examples/README.md) for details.
547
479
 
548
480
  ### Running Examples
549
481
 
482
+ Run these commands from the cloned repository's `typescript` directory:
483
+
550
484
  ```bash
551
485
  # Set environment variables
552
486
  export ZEROBUS_SERVER_ENDPOINT="https://<workspace-id>.zerobus.<region>.cloud.databricks.com"
@@ -576,7 +510,7 @@ For higher throughput, use batch ingestion to send multiple records with a singl
576
510
 
577
511
  ```typescript
578
512
  const records = Array.from({ length: 1000 }, (_, i) =>
579
- AirQuality.create({ device_name: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
513
+ AirQuality.create({ deviceName: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
580
514
  );
581
515
 
582
516
  // Protobuf Type 1: Message objects (high-level) - SDK auto-serializes
@@ -659,7 +593,7 @@ const stream = await sdk.createStream(
659
593
  '', // client_secret (ignored when headers_provider is provided)
660
594
  options,
661
595
  {
662
- getHeadersCallback: async () => [
596
+ getHeadersCallback: () => [
663
597
  ["authorization", `Bearer ${myToken}`],
664
598
  ["x-databricks-zerobus-table-name", tableName]
665
599
  ]
@@ -680,7 +614,7 @@ const stream = await sdk.createStream(
680
614
  | Option | Default | Description |
681
615
  |--------|---------|-------------|
682
616
  | `recordType` | `RecordType.Proto` | Serialization format: `RecordType.Json` or `RecordType.Proto` |
683
- | `maxInflightRequests` | 10,000 | Maximum number of unacknowledged requests |
617
+ | `maxInflightRequests` | 1,000,000 | Maximum number of unacknowledged requests |
684
618
  | `recovery` | true | Enable automatic stream recovery |
685
619
  | `recoveryTimeoutMs` | 15,000 | Timeout for recovery operations (ms) |
686
620
  | `recoveryBackoffMs` | 2,000 | Delay between recovery attempts (ms) |
@@ -714,13 +648,15 @@ const stream = await sdk.createStream(
714
648
  ## Descriptor Utilities
715
649
 
716
650
  The SDK provides a helper function to extract Protocol Buffer descriptors from FileDescriptorSets.
651
+ Use the `.js` subpath shown below for compatibility with CommonJS and native
652
+ Node.js ESM imports.
717
653
 
718
654
  ### loadDescriptorProto()
719
655
 
720
656
  Extracts a specific message descriptor from a FileDescriptorSet:
721
657
 
722
658
  ```typescript
723
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
659
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
724
660
 
725
661
  const descriptorBase64 = loadDescriptorProto({
726
662
  descriptorPath: 'schemas/my_schema_descriptor.pb',
@@ -753,38 +689,37 @@ const descriptorBase64 = loadDescriptorProto({
753
689
 
754
690
  ## Error Handling
755
691
 
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:
692
+ 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
693
 
758
694
  ```typescript
695
+ let replacement;
759
696
  try {
760
697
  const offset = await stream.ingestRecordOffset(record);
761
- await stream.waitForOffset(offset);
698
+ await stream.flush();
762
699
  console.log(`Success: offset ${offset}`);
763
700
  } catch (error) {
764
701
  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
702
  try {
785
- // Continue ingesting...
703
+ const unackedBatches = await stream.getUnackedBatches();
704
+ console.log(`Batches to recover: ${unackedBatches.length}`);
705
+ replacement = await sdk.recreateStream(stream);
706
+ await replacement.flush();
707
+ } catch (recoveryError) {
708
+ console.error('Stream was not terminal or recovery failed:', recoveryError);
709
+ throw new AggregateError(
710
+ [error, recoveryError],
711
+ 'ingestion and recovery both failed',
712
+ );
786
713
  } finally {
787
- await newStream.close();
714
+ if (replacement) {
715
+ await replacement.close();
716
+ }
717
+ }
718
+ } finally {
719
+ try {
720
+ await stream.close();
721
+ } catch (closeError) {
722
+ console.error('Failed stream released:', closeError);
788
723
  }
789
724
  }
790
725
  ```
@@ -804,12 +739,14 @@ Main entry point for the SDK.
804
739
  **Constructor:**
805
740
 
806
741
  ```typescript
807
- new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string)
742
+ new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string, options?: ZerobusSdkOptions)
808
743
  ```
809
744
 
810
745
  **Parameters:**
811
746
  - `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
747
  - `unityCatalogUrl` (string) - The Unity Catalog endpoint (your workspace URL)
748
+ - `options` (ZerobusSdkOptions, optional) - Additional SDK configuration:
749
+ - `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
750
 
814
751
  **Methods:**
815
752
 
@@ -845,23 +782,29 @@ This method is the **recommended approach** for recovering from stream failures.
845
782
  4. Returns the new stream ready for continued ingestion
846
783
 
847
784
  **Parameters:**
848
- - `stream` - The failed or closed stream to recreate
785
+ - `stream` - The terminally failed stream to recreate. Do not call `stream.close()`
786
+ first because the TypeScript wrapper releases its native handle on close.
849
787
 
850
788
  **Returns:** Promise resolving to a new `ZerobusStream` with all unacknowledged batches re-ingested
851
789
 
852
790
  **Example:**
853
791
  ```typescript
854
792
  try {
855
- await stream.ingestRecords(batch);
793
+ await stream.ingestRecordsOffset(batch);
794
+ await stream.flush();
856
795
  } catch (error) {
857
- await stream.close();
858
- // Automatically recreate stream and recover all unacked batches
796
+ // recreateStream() rejects unless the native stream already failed closed.
859
797
  const newStream = await sdk.recreateStream(stream);
860
- // Continue ingesting with newStream
798
+ try {
799
+ await newStream.flush();
800
+ } finally {
801
+ await newStream.close();
802
+ }
861
803
  }
862
804
  ```
863
805
 
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.
806
+ **Note:** This method preserves batch structure and re-ingests batches atomically. For
807
+ debugging, inspect `getUnackedBatches()` after a terminal failure and before closing the wrapper.
865
808
 
866
809
  ---
867
810
 
@@ -875,13 +818,16 @@ Represents an active ingestion stream.
875
818
  async ingestRecordOffset(payload: Buffer | string | object): Promise<bigint>
876
819
  ```
877
820
 
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.
821
+ **(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
822
 
880
823
  ```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
824
+ // Idiomatic flow: ingest in a loop, then flush once
825
+ let lastOffset: bigint | null = null;
826
+ for (const record of records) {
827
+ lastOffset = await stream.ingestRecordOffset(record); // Resolves immediately
828
+ }
829
+ await stream.flush(); // Resolves once everything queued so far is acknowledged
830
+ // (Or, to confirm a specific record: if (lastOffset !== null) await stream.waitForOffset(lastOffset))
885
831
  ```
886
832
 
887
833
  ---
@@ -890,7 +836,7 @@ await stream.waitForOffset(offset2); // Waits for server to acknowledge all rec
890
836
  async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bigint | null>
891
837
  ```
892
838
 
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.
839
+ **(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
840
 
895
841
  ---
896
842
 
@@ -898,7 +844,7 @@ async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bi
898
844
  async waitForOffset(offsetId: bigint): Promise<void>
899
845
  ```
900
846
 
901
- Waits for the server to acknowledge all records up to and including the specified offset ID.
847
+ 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
848
 
903
849
  ---
904
850
 
@@ -998,7 +944,7 @@ await stream.ingestRecords(buffers);
998
944
  async flush(): Promise<void>
999
945
  ```
1000
946
 
1001
- Flushes all pending records and waits for acknowledgments.
947
+ 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
948
 
1003
949
  ```typescript
1004
950
  async close(): Promise<void>
@@ -1012,7 +958,8 @@ async getUnackedRecords(): Promise<Buffer[]>
1012
958
 
1013
959
  Returns unacknowledged record payloads as a flat array for inspection purposes.
1014
960
 
1015
- **Important:** Can only be called on **closed streams**. Call `stream.close()` first, or this will throw an error.
961
+ **Important:** This can only be called after a terminal stream failure. Do not call
962
+ `stream.close()` first: the TypeScript wrapper releases the underlying stream handle on close.
1016
963
 
1017
964
  **Returns:** Array of Buffer containing the raw record payloads
1018
965
 
@@ -1026,7 +973,8 @@ async getUnackedBatches(): Promise<Buffer[][]>
1026
973
 
1027
974
  Returns unacknowledged records grouped by their original batches for inspection purposes.
1028
975
 
1029
- **Important:** Can only be called on **closed streams**. Call `stream.close()` first, or this will throw an error.
976
+ **Important:** This can only be called after a terminal stream failure. Do not call
977
+ `stream.close()` first: the TypeScript wrapper releases the underlying stream handle on close.
1030
978
 
1031
979
  **Returns:** Array of arrays, where each inner array represents a batch of records as Buffers
1032
980
 
@@ -1039,15 +987,11 @@ try {
1039
987
  await stream.ingestRecords(batch2);
1040
988
  // ... error occurs
1041
989
  } catch (error) {
1042
- await stream.close();
1043
990
  const unackedBatches = await stream.getUnackedBatches();
1044
991
  // unackedBatches[0] contains records from batch1 (if not acked)
1045
992
  // unackedBatches[1] contains records from batch2 (if not acked)
1046
993
 
1047
- // Re-ingest with new stream
1048
- for (const batch of unackedBatches) {
1049
- await newStream.ingestRecords(batch);
1050
- }
994
+ console.log(`Batches available for recovery: ${unackedBatches.length}`);
1051
995
  }
1052
996
  ```
1053
997
 
@@ -1070,10 +1014,10 @@ interface TableProperties {
1070
1014
 
1071
1015
  ```typescript
1072
1016
  // JSON mode
1073
- const tableProperties = { tableName: 'main.default.air_quality' };
1017
+ const jsonTableProperties = { tableName: 'main.default.air_quality' };
1074
1018
 
1075
1019
  // Protocol Buffers mode
1076
- const tableProperties = {
1020
+ const protoTableProperties = {
1077
1021
  tableName: 'main.default.air_quality',
1078
1022
  descriptorProto: descriptorBase64 // Required for protobuf
1079
1023
  };
@@ -1090,7 +1034,7 @@ Configuration options for stream behavior.
1090
1034
  ```typescript
1091
1035
  interface StreamConfigurationOptions {
1092
1036
  recordType?: RecordType; // RecordType.Json or RecordType.Proto. Default: RecordType.Proto
1093
- maxInflightRequests?: number; // Default: 10,000
1037
+ maxInflightRequests?: number; // Default: 1,000,000
1094
1038
  recovery?: boolean; // Default: true
1095
1039
  recoveryTimeoutMs?: number; // Default: 15,000
1096
1040
  recoveryBackoffMs?: number; // Default: 2,000
@@ -1110,11 +1054,12 @@ enum RecordType {
1110
1054
 
1111
1055
  1. **Reuse SDK instances**: Create one `ZerobusSdk` instance per application
1112
1056
  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)
1057
+ 3. **Batch size**: Adjust `maxInflightRequests` based on your throughput requirements (default: 1,000,000)
1114
1058
  4. **Error handling**: The stream handles errors internally with automatic retry. Only use `recreateStream()` for persistent failures after internal retries are exhausted.
1115
1059
  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
1060
  6. **Store credentials securely**: Use environment variables, never hardcode credentials
1117
1061
  7. **Use batch ingestion**: For high-throughput scenarios, use `ingestRecordsOffset()` instead of individual `ingestRecordOffset()` calls
1062
+ 8. **Ingest in a loop, then `flush()`**: See [Acknowledgments and throughput](#acknowledgments-and-throughput) above for the full explanation.
1118
1063
 
1119
1064
  ## Platform Support
1120
1065
 
@@ -1161,7 +1106,7 @@ This SDK wraps the high-performance [Rust Zerobus SDK](https://github.com/databr
1161
1106
  **Benefits:**
1162
1107
  - **Native performance** - Rust implementation for high-throughput ingestion
1163
1108
  - **Native async/await support** - Rust futures become JavaScript Promises
1164
- - **Automatic memory management** - No manual cleanup required
1109
+ - **Automatic memory management** for native objects. You still must `await stream.close()` to flush and release the stream.
1165
1110
  - **Type safety** - Compile-time checks on both sides
1166
1111
 
1167
1112
  ## Community and Contributing