@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/Cargo.lock +82 -97
- package/Cargo.toml +5 -12
- package/README.md +147 -203
- package/index.d.ts +82 -38
- package/package.json +28 -13
- package/src/headers_provider.ts +24 -36
- package/src/lib.rs +113 -61
- package/utils/descriptor.d.ts +32 -0
- package/utils/descriptor.js +55 -0
- package/utils/descriptor.ts +2 -2
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
|
-
|
|
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
|
-
|
|
63
|
+
npm install @databricks/zerobus-ingest-sdk
|
|
78
64
|
```
|
|
79
65
|
|
|
80
|
-
|
|
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
|
-
|
|
69
|
+
#### Local Development From Source
|
|
85
70
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 = '
|
|
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
|
-
//
|
|
237
|
-
|
|
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.
|
|
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**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
+
deviceName: `sensor-${i}`,
|
|
397
327
|
temp: 20 + i,
|
|
398
328
|
humidity: 50 + i
|
|
399
329
|
});
|
|
400
330
|
|
|
401
|
-
//
|
|
402
|
-
|
|
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.
|
|
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. **
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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
|
-
|
|
446
|
+
After creating `schemas/air_quality.proto` and the `examples/generated`
|
|
447
|
+
directory as described above:
|
|
517
448
|
```bash
|
|
518
|
-
# Install
|
|
519
|
-
npm install
|
|
520
|
-
npm
|
|
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
|
-
#
|
|
523
|
-
|
|
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** (`
|
|
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
|
-
|
|
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({
|
|
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:
|
|
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` |
|
|
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`).
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
792
|
+
await stream.ingestRecordsOffset(batch);
|
|
793
|
+
await stream.flush();
|
|
856
794
|
} catch (error) {
|
|
857
|
-
|
|
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
|
-
|
|
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
|
|
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).
|
|
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
|
-
//
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
await stream.
|
|
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:**
|
|
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:**
|
|
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
|
-
|
|
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
|
|
1016
|
+
const jsonTableProperties = { tableName: 'main.default.air_quality' };
|
|
1074
1017
|
|
|
1075
1018
|
// Protocol Buffers mode
|
|
1076
|
-
const
|
|
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:
|
|
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:
|
|
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**
|
|
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
|