@databricks/zerobus-ingest-sdk 1.0.2 → 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
@@ -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 and Protocol Buffers
31
+ - **Multiple serialization formats**: Support for JSON, Protocol Buffers, and Arrow Flight (Beta) 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,110 +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
- **Note for macOS users**: Pre-built binaries are not available. The package will automatically build from source during `npm install`. Ensure you have Rust toolchain and Xcode Command Line Tools installed (see prerequisites above).
141
-
142
- 1. Clone the repository:
143
- ```bash
144
- git clone https://github.com/databricks/zerobus-sdk.git
145
- cd zerobus-sdk/ts
146
- ```
147
-
148
- 2. Install dependencies:
149
- ```bash
150
- npm install
151
- ```
152
-
153
- 3. Build the native addon:
154
- ```bash
155
- npm run build
156
- ```
157
-
158
- This will compile the Rust code into a native Node.js addon (`.node` file) for your platform.
159
-
160
- 4. Verify the build:
161
- ```bash
162
- # You should see a .node file
163
- ls -la *.node
164
- ```
165
-
166
- 5. The SDK is now ready to use! You can:
167
- - Use it directly in this directory for examples
168
- - Link it globally: `npm link`
169
- - Or copy it into your project's `node_modules`
170
-
171
81
  **Troubleshooting:**
172
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`
173
86
  - **"rustc: command not found"**: Restart your terminal after installing Rust
174
87
  - **Build fails on Windows**: Ensure Visual Studio Build Tools are installed with C++ support
175
88
  - **Build fails on Linux**: Install build-essential or equivalent package
@@ -184,6 +97,10 @@ The SDK supports two serialization formats. **Protocol Buffers is the default**
184
97
 
185
98
  > **Note:** If you don't specify `recordType`, the SDK will use Protocol Buffers by default. To use JSON, explicitly set `recordType: RecordType.Json`.
186
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
+
187
104
  ### Option 1: Using JSON (Quick Start)
188
105
 
189
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`.
@@ -191,12 +108,13 @@ JSON mode is the simplest way to get started. You don't need to define or compil
191
108
  ```typescript
192
109
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
193
110
 
111
+ async function main(): Promise<void> {
194
112
  // Configuration
195
113
  // For AWS:
196
114
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
197
115
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
198
116
  // For Azure:
199
- // const zerobusEndpoint = '<workspace-id>.zerobus.<region>.azuredatabricks.net';
117
+ // const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.azuredatabricks.net';
200
118
  // const workspaceUrl = 'https://<workspace-name>.azuredatabricks.net';
201
119
 
202
120
  const tableName = 'main.default.air_quality';
@@ -225,8 +143,6 @@ const stream = await sdk.createStream(
225
143
  );
226
144
 
227
145
  try {
228
- let lastOffset: bigint;
229
-
230
146
  // Send all records
231
147
  for (let i = 0; i < 100; i++) {
232
148
  const record = {
@@ -235,16 +151,23 @@ try {
235
151
  humidity: 50 + (i % 40)
236
152
  };
237
153
 
238
- // ingestRecordOffset returns immediately after queuing
239
- lastOffset = await stream.ingestRecordOffset(record);
154
+ // Queue the record; do not wait for its acknowledgement here
155
+ await stream.ingestRecordOffset(record);
240
156
  }
241
157
 
242
158
  // Wait for all records to be acknowledged
243
- await stream.waitForOffset(lastOffset);
159
+ await stream.flush();
244
160
  console.log('Successfully ingested 100 records!');
245
161
  } finally {
246
162
  await stream.close();
247
163
  }
164
+
165
+ }
166
+
167
+ main().catch((error) => {
168
+ console.error('Fatal error:', error);
169
+ process.exitCode = 1;
170
+ });
248
171
  ```
249
172
 
250
173
  ### Option 2: Using Protocol Buffers (Default, Recommended)
@@ -256,7 +179,15 @@ Protocol Buffers is the default serialization format and provides efficient bina
256
179
  Before starting, ensure you have:
257
180
 
258
181
  1. **Protocol Buffer Compiler (`protoc`)** - Required for generating descriptor files
259
- 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
+ ```
260
191
 
261
192
  #### Step 1: Install Protocol Buffer Compiler
262
193
 
@@ -297,7 +228,9 @@ protoc --version
297
228
 
298
229
  #### Step 2: Define Your Protocol Buffer Schema
299
230
 
300
- 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:
301
234
 
302
235
  ```protobuf
303
236
  syntax = "proto2";
@@ -317,13 +250,8 @@ message AirQuality {
317
250
  Generate TypeScript code from your proto schema:
318
251
 
319
252
  ```bash
320
- npm run build:proto
321
- ```
322
-
323
- This runs:
324
- ```bash
325
- pbjs -t static-module -w commonjs -o examples/generated/air_quality.js schemas/air_quality.proto
326
- 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
327
255
  ```
328
256
 
329
257
  **Output:**
@@ -353,8 +281,9 @@ That's it! The SDK will automatically extract the message descriptor from this f
353
281
  ```typescript
354
282
  import { ZerobusSdk, RecordType } from '@databricks/zerobus-ingest-sdk';
355
283
  import * as airQuality from './examples/generated/air_quality';
356
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
284
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
357
285
 
286
+ async function main(): Promise<void> {
358
287
  // Configuration
359
288
  const zerobusEndpoint = 'https://<workspace-id>.zerobus.<region>.cloud.databricks.com';
360
289
  const workspaceUrl = 'https://<workspace-name>.cloud.databricks.com';
@@ -390,26 +319,32 @@ const stream = await sdk.createStream(tableProperties, clientId, clientSecret, o
390
319
 
391
320
  try {
392
321
  const AirQuality = airQuality.examples.AirQuality;
393
- let lastOffset: bigint;
394
322
 
395
323
  // Send all records
396
324
  for (let i = 0; i < 100; i++) {
397
325
  const record = AirQuality.create({
398
- device_name: `sensor-${i}`,
326
+ deviceName: `sensor-${i}`,
399
327
  temp: 20 + i,
400
328
  humidity: 50 + i
401
329
  });
402
330
 
403
- // ingestRecordOffset returns immediately after queuing
404
- lastOffset = await stream.ingestRecordOffset(record);
331
+ // Queue the record; do not wait for its acknowledgement here
332
+ await stream.ingestRecordOffset(record);
405
333
  }
406
334
 
407
335
  // Wait for all records to be acknowledged
408
- await stream.waitForOffset(lastOffset);
336
+ await stream.flush();
409
337
  console.log('Successfully ingested 100 records!');
410
338
  } finally {
411
339
  await stream.close();
412
340
  }
341
+
342
+ }
343
+
344
+ main().catch((error) => {
345
+ console.error('Fatal error:', error);
346
+ process.exitCode = 1;
347
+ });
413
348
  ```
414
349
 
415
350
  #### Type Mapping: Delta ↔ Protocol Buffers
@@ -468,24 +403,16 @@ message NestedData {
468
403
  EOF
469
404
  ```
470
405
 
471
- 2. **Add build script to package.json:**
472
- ```json
473
- {
474
- "scripts": {
475
- "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"
476
- }
477
- }
478
- ```
479
-
480
- 3. **Generate code and descriptor:**
406
+ 2. **Generate code and descriptor:**
481
407
  ```bash
482
- 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
483
410
  protoc --descriptor_set_out=schemas/my_schema_descriptor.pb --include_imports schemas/my_schema.proto
484
411
  ```
485
412
 
486
- 4. **Load descriptor in your code:**
413
+ 3. **Load descriptor in your code:**
487
414
  ```typescript
488
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
415
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
489
416
  const descriptorBase64 = loadDescriptorProto({
490
417
  descriptorPath: 'schemas/my_schema_descriptor.pb',
491
418
  protoFileName: 'my_schema.proto',
@@ -499,7 +426,7 @@ message NestedData {
499
426
  - Install `protoc` (see Step 1 above)
500
427
 
501
428
  **"Cannot find module './generated/air_quality'"**
502
- - Run `npm run build:proto` to generate TypeScript code
429
+ - Run the `npx pbjs` and `npx pbts` commands from Step 3
503
430
 
504
431
  **"Descriptor file not found"**
505
432
  - Generate the descriptor file using the commands in Step 4
@@ -511,27 +438,27 @@ message NestedData {
511
438
  - Make sure you're using `loadDescriptorProto()` from the utils
512
439
 
513
440
  **Build fails on proto generation**
514
- - 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`
515
443
 
516
444
  #### Quick Reference
517
445
 
518
- Complete setup from scratch:
446
+ After creating `schemas/air_quality.proto` and the `examples/generated`
447
+ directory as described above:
519
448
  ```bash
520
- # Install dependencies and build SDK
521
- npm install
522
- 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
523
452
 
524
- # Setup Protocol Buffers
525
- 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
526
456
  protoc --descriptor_set_out=schemas/air_quality_descriptor.pb --include_imports schemas/air_quality.proto
527
-
528
- # Run example
529
- npm run example:proto:single
530
457
  ```
531
458
 
532
459
  #### Why Two Steps (TypeScript + Descriptor)?
533
460
 
534
- 1. **TypeScript Code Generation** (`npm run build:proto`):
461
+ 1. **TypeScript Code Generation** (`npx pbjs` and `npx pbts`):
535
462
  - Creates JavaScript/TypeScript code for your application
536
463
  - Provides type-safe message creation and encoding
537
464
  - Used in your application code
@@ -545,10 +472,14 @@ Both are necessary for Protocol Buffers ingestion!
545
472
 
546
473
  ## Usage Examples
547
474
 
548
- 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.
549
478
 
550
479
  ### Running Examples
551
480
 
481
+ Run these commands from the cloned repository's `typescript` directory:
482
+
552
483
  ```bash
553
484
  # Set environment variables
554
485
  export ZEROBUS_SERVER_ENDPOINT="https://<workspace-id>.zerobus.<region>.cloud.databricks.com"
@@ -578,7 +509,7 @@ For higher throughput, use batch ingestion to send multiple records with a singl
578
509
 
579
510
  ```typescript
580
511
  const records = Array.from({ length: 1000 }, (_, i) =>
581
- AirQuality.create({ device_name: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
512
+ AirQuality.create({ deviceName: `sensor-${i}`, temp: 20 + i, humidity: 50 + i })
582
513
  );
583
514
 
584
515
  // Protobuf Type 1: Message objects (high-level) - SDK auto-serializes
@@ -661,7 +592,7 @@ const stream = await sdk.createStream(
661
592
  '', // client_secret (ignored when headers_provider is provided)
662
593
  options,
663
594
  {
664
- getHeadersCallback: async () => [
595
+ getHeadersCallback: () => [
665
596
  ["authorization", `Bearer ${myToken}`],
666
597
  ["x-databricks-zerobus-table-name", tableName]
667
598
  ]
@@ -682,7 +613,7 @@ const stream = await sdk.createStream(
682
613
  | Option | Default | Description |
683
614
  |--------|---------|-------------|
684
615
  | `recordType` | `RecordType.Proto` | Serialization format: `RecordType.Json` or `RecordType.Proto` |
685
- | `maxInflightRequests` | 10,000 | Maximum number of unacknowledged requests |
616
+ | `maxInflightRequests` | 1,000,000 | Maximum number of unacknowledged requests |
686
617
  | `recovery` | true | Enable automatic stream recovery |
687
618
  | `recoveryTimeoutMs` | 15,000 | Timeout for recovery operations (ms) |
688
619
  | `recoveryBackoffMs` | 2,000 | Delay between recovery attempts (ms) |
@@ -716,13 +647,15 @@ const stream = await sdk.createStream(
716
647
  ## Descriptor Utilities
717
648
 
718
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.
719
652
 
720
653
  ### loadDescriptorProto()
721
654
 
722
655
  Extracts a specific message descriptor from a FileDescriptorSet:
723
656
 
724
657
  ```typescript
725
- import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor';
658
+ import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
726
659
 
727
660
  const descriptorBase64 = loadDescriptorProto({
728
661
  descriptorPath: 'schemas/my_schema_descriptor.pb',
@@ -755,38 +688,37 @@ const descriptorBase64 = loadDescriptorProto({
755
688
 
756
689
  ## Error Handling
757
690
 
758
- 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.
759
692
 
760
693
  ```typescript
694
+ let replacement;
761
695
  try {
762
696
  const offset = await stream.ingestRecordOffset(record);
763
- await stream.waitForOffset(offset);
697
+ await stream.flush();
764
698
  console.log(`Success: offset ${offset}`);
765
699
  } catch (error) {
766
700
  console.error('Ingestion failed:', error);
767
-
768
- // When stream fails, close it first
769
- await stream.close();
770
- console.log('Stream closed after error');
771
-
772
- // Optional: Inspect what needs recovery (must be called on closed stream)
773
- const unackedBatches = await stream.getUnackedBatches();
774
- console.log(`Batches to recover: ${unackedBatches.length}`);
775
-
776
- // Recommended recovery approach: Use recreateStream()
777
- // This method:
778
- // 1. Gets all unacknowledged batches from the failed stream
779
- // 2. Creates a new stream with the same configuration
780
- // 3. Re-ingests all unacknowledged batches automatically
781
- // 4. Returns the new stream ready for continued use
782
- const newStream = await sdk.recreateStream(stream);
783
- console.log(`Stream recreated with ${unackedBatches.length} batches re-ingested`);
784
-
785
- // Continue using newStream for further ingestion
786
701
  try {
787
- // 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
+ );
788
712
  } finally {
789
- 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);
790
722
  }
791
723
  }
792
724
  ```
@@ -806,12 +738,14 @@ Main entry point for the SDK.
806
738
  **Constructor:**
807
739
 
808
740
  ```typescript
809
- new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string)
741
+ new ZerobusSdk(zerobusEndpoint: string, unityCatalogUrl: string, options?: ZerobusSdkOptions)
810
742
  ```
811
743
 
812
744
  **Parameters:**
813
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)
814
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.
815
749
 
816
750
  **Methods:**
817
751
 
@@ -847,23 +781,29 @@ This method is the **recommended approach** for recovering from stream failures.
847
781
  4. Returns the new stream ready for continued ingestion
848
782
 
849
783
  **Parameters:**
850
- - `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.
851
786
 
852
787
  **Returns:** Promise resolving to a new `ZerobusStream` with all unacknowledged batches re-ingested
853
788
 
854
789
  **Example:**
855
790
  ```typescript
856
791
  try {
857
- await stream.ingestRecords(batch);
792
+ await stream.ingestRecordsOffset(batch);
793
+ await stream.flush();
858
794
  } catch (error) {
859
- await stream.close();
860
- // Automatically recreate stream and recover all unacked batches
795
+ // recreateStream() rejects unless the native stream already failed closed.
861
796
  const newStream = await sdk.recreateStream(stream);
862
- // Continue ingesting with newStream
797
+ try {
798
+ await newStream.flush();
799
+ } finally {
800
+ await newStream.close();
801
+ }
863
802
  }
864
803
  ```
865
804
 
866
- **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.
867
807
 
868
808
  ---
869
809
 
@@ -877,13 +817,16 @@ Represents an active ingestion stream.
877
817
  async ingestRecordOffset(payload: Buffer | string | object): Promise<bigint>
878
818
  ```
879
819
 
880
- **(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.
881
821
 
882
822
  ```typescript
883
- // High-throughput pattern: send many, wait once
884
- const offset1 = await stream.ingestRecordOffset(record1); // Resolves immediately
885
- const offset2 = await stream.ingestRecordOffset(record2); // Resolves immediately
886
- 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))
887
830
  ```
888
831
 
889
832
  ---
@@ -892,7 +835,7 @@ await stream.waitForOffset(offset2); // Waits for server to acknowledge all rec
892
835
  async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bigint | null>
893
836
  ```
894
837
 
895
- **(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.
896
839
 
897
840
  ---
898
841
 
@@ -900,7 +843,7 @@ async ingestRecordsOffset(payloads: Array<Buffer | string | object>): Promise<bi
900
843
  async waitForOffset(offsetId: bigint): Promise<void>
901
844
  ```
902
845
 
903
- 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.
904
847
 
905
848
  ---
906
849
 
@@ -1000,7 +943,7 @@ await stream.ingestRecords(buffers);
1000
943
  async flush(): Promise<void>
1001
944
  ```
1002
945
 
1003
- 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.
1004
947
 
1005
948
  ```typescript
1006
949
  async close(): Promise<void>
@@ -1014,7 +957,8 @@ async getUnackedRecords(): Promise<Buffer[]>
1014
957
 
1015
958
  Returns unacknowledged record payloads as a flat array for inspection purposes.
1016
959
 
1017
- **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.
1018
962
 
1019
963
  **Returns:** Array of Buffer containing the raw record payloads
1020
964
 
@@ -1028,7 +972,8 @@ async getUnackedBatches(): Promise<Buffer[][]>
1028
972
 
1029
973
  Returns unacknowledged records grouped by their original batches for inspection purposes.
1030
974
 
1031
- **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.
1032
977
 
1033
978
  **Returns:** Array of arrays, where each inner array represents a batch of records as Buffers
1034
979
 
@@ -1041,15 +986,11 @@ try {
1041
986
  await stream.ingestRecords(batch2);
1042
987
  // ... error occurs
1043
988
  } catch (error) {
1044
- await stream.close();
1045
989
  const unackedBatches = await stream.getUnackedBatches();
1046
990
  // unackedBatches[0] contains records from batch1 (if not acked)
1047
991
  // unackedBatches[1] contains records from batch2 (if not acked)
1048
992
 
1049
- // Re-ingest with new stream
1050
- for (const batch of unackedBatches) {
1051
- await newStream.ingestRecords(batch);
1052
- }
993
+ console.log(`Batches available for recovery: ${unackedBatches.length}`);
1053
994
  }
1054
995
  ```
1055
996
 
@@ -1072,10 +1013,10 @@ interface TableProperties {
1072
1013
 
1073
1014
  ```typescript
1074
1015
  // JSON mode
1075
- const tableProperties = { tableName: 'main.default.air_quality' };
1016
+ const jsonTableProperties = { tableName: 'main.default.air_quality' };
1076
1017
 
1077
1018
  // Protocol Buffers mode
1078
- const tableProperties = {
1019
+ const protoTableProperties = {
1079
1020
  tableName: 'main.default.air_quality',
1080
1021
  descriptorProto: descriptorBase64 // Required for protobuf
1081
1022
  };
@@ -1092,7 +1033,7 @@ Configuration options for stream behavior.
1092
1033
  ```typescript
1093
1034
  interface StreamConfigurationOptions {
1094
1035
  recordType?: RecordType; // RecordType.Json or RecordType.Proto. Default: RecordType.Proto
1095
- maxInflightRequests?: number; // Default: 10,000
1036
+ maxInflightRequests?: number; // Default: 1,000,000
1096
1037
  recovery?: boolean; // Default: true
1097
1038
  recoveryTimeoutMs?: number; // Default: 15,000
1098
1039
  recoveryBackoffMs?: number; // Default: 2,000
@@ -1112,11 +1053,12 @@ enum RecordType {
1112
1053
 
1113
1054
  1. **Reuse SDK instances**: Create one `ZerobusSdk` instance per application
1114
1055
  2. **Stream lifecycle**: Always close streams in a `finally` block to ensure all records are flushed
1115
- 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)
1116
1057
  4. **Error handling**: The stream handles errors internally with automatic retry. Only use `recreateStream()` for persistent failures after internal retries are exhausted.
1117
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.
1118
1059
  6. **Store credentials securely**: Use environment variables, never hardcode credentials
1119
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.
1120
1062
 
1121
1063
  ## Platform Support
1122
1064
 
@@ -1127,14 +1069,15 @@ The SDK supports all platforms where Node.js and Rust are available.
1127
1069
  Pre-built native binaries are available for:
1128
1070
 
1129
1071
  - **Linux**: x64, ARM64
1072
+ - **macOS**: x64, ARM64
1130
1073
  - **Windows**: x64
1131
1074
 
1132
1075
  ### Build from Source
1133
1076
 
1134
- **macOS users**: Pre-built binaries are not available for macOS. The package will automatically build from source during `npm install`, which requires:
1077
+ Other platforms (e.g. Linux musl, FreeBSD) need to build from source during `npm install`, which requires:
1135
1078
 
1136
1079
  - **Rust toolchain** (1.70+): Install via `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
1137
- - **Xcode Command Line Tools**: Install via `xcode-select --install`
1080
+ - Platform C/C++ build tools (Xcode CLT on macOS, build-essential on Debian/Ubuntu, etc.)
1138
1081
 
1139
1082
  The build process happens automatically during installation and typically takes 2-3 minutes.
1140
1083
 
@@ -1162,7 +1105,7 @@ This SDK wraps the high-performance [Rust Zerobus SDK](https://github.com/databr
1162
1105
  **Benefits:**
1163
1106
  - **Native performance** - Rust implementation for high-throughput ingestion
1164
1107
  - **Native async/await support** - Rust futures become JavaScript Promises
1165
- - **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.
1166
1109
  - **Type safety** - Compile-time checks on both sides
1167
1110
 
1168
1111
  ## Community and Contributing