@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.
@@ -1,69 +1,57 @@
1
1
  /**
2
- * Interface for providing custom headers to Zerobus streams.
2
+ * Custom headers provider accepted by `createStream()`.
3
3
  *
4
- * Implement this interface to use custom authentication beyond OAuth,
5
- * such as Personal Access Tokens (PAT) or custom auth tokens.
4
+ * The native adapter invokes `getHeadersCallback` synchronously once during
5
+ * stream creation and stores the returned tuples. Returning a Promise, or
6
+ * passing a class with async `getHeaders()`, is not supported and can terminate
7
+ * the process. Token refresh is not currently wired through this callback.
6
8
  */
7
9
  export interface HeadersProvider {
8
10
  /**
9
- * Returns headers as array of [name, value] tuples.
11
+ * Returns headers as an array of [name, value] tuples.
10
12
  *
11
13
  * Required headers:
12
14
  * - ["authorization", "Bearer <token>"]
13
15
  * - ["x-databricks-zerobus-table-name", "<table_name>"]
14
- *
15
- * @returns Promise resolving to array of header name-value pairs
16
16
  */
17
- getHeaders(): Promise<Array<[string, string]>>;
17
+ getHeadersCallback: () => Array<[string, string]>;
18
18
  }
19
19
 
20
20
  /**
21
21
  * OAuth 2.0 Client Credentials headers provider.
22
22
  *
23
- * **IMPORTANT: DO NOT instantiate this class directly.**
23
+ * Do not instantiate this class or pass it to `createStream()`.
24
24
  *
25
25
  * OAuth authentication is handled automatically by the Rust SDK when you call
26
- * `createStream()` with clientId and clientSecret parameters (without providing
27
- * a headers_provider).
28
- *
29
- * This class exists for:
30
- * 1. Documentation purposes - showing the HeadersProvider pattern
31
- * 2. API consistency with other Zerobus SDKs (Python, Java, Rust)
26
+ * `createStream()` with clientId and clientSecret and omit the headers provider.
32
27
  *
33
- * **How to use OAuth authentication:**
28
+ * How to use OAuth authentication:
34
29
  * ```typescript
35
- * // OAuth is the default - just pass clientId and clientSecret
36
30
  * const stream = await sdk.createStream(
37
31
  * tableProperties,
38
- * clientId, // OAuth client ID
39
- * clientSecret, // OAuth client secret
32
+ * clientId,
33
+ * clientSecret,
40
34
  * options
41
- * // No headers_provider parameter = OAuth authentication
42
35
  * );
43
36
  * ```
44
37
  *
45
- * **How to use custom authentication (PAT, etc.):**
38
+ * How to use custom authentication (PAT or a static token):
46
39
  * ```typescript
47
- * class CustomHeadersProvider implements HeadersProvider {
48
- * async getHeaders() {
49
- * return [
50
- * ["authorization", `Bearer ${myToken}`],
51
- * ["x-databricks-zerobus-table-name", tableName]
52
- * ];
53
- * }
54
- * }
55
- *
56
- * const provider = new CustomHeadersProvider();
57
40
  * const stream = await sdk.createStream(
58
41
  * tableProperties,
59
- * '', // ignored
60
- * '', // ignored
42
+ * '',
43
+ * '',
61
44
  * options,
62
- * { getHeadersCallback: provider.getHeaders.bind(provider) }
45
+ * {
46
+ * getHeadersCallback: () => [
47
+ * ["authorization", `Bearer ${myToken}`],
48
+ * ["x-databricks-zerobus-table-name", tableName]
49
+ * ]
50
+ * }
63
51
  * );
64
52
  * ```
65
53
  */
66
- export class OAuthHeadersProvider implements HeadersProvider {
54
+ export class OAuthHeadersProvider {
67
55
  constructor(
68
56
  private clientId: string,
69
57
  private clientSecret: string,
@@ -75,8 +63,8 @@ export class OAuthHeadersProvider implements HeadersProvider {
75
63
  throw new Error(
76
64
  'OAuthHeadersProvider should not be instantiated directly. ' +
77
65
  'OAuth authentication is handled internally by the Rust SDK. ' +
78
- 'To use OAuth: call createStream(tableProperties, clientId, clientSecret, options) without the headers_provider parameter. ' +
79
- 'To use custom authentication: implement the HeadersProvider interface.'
66
+ 'To use OAuth: call createStream(tableProperties, clientId, clientSecret, options) without a headers provider. ' +
67
+ 'To use custom authentication: pass { getHeadersCallback: () => [...] } as the headers provider.'
80
68
  );
81
69
  }
82
70
  }
package/src/lib.rs CHANGED
@@ -50,7 +50,7 @@ pub enum RecordType {
50
50
  #[napi(object)]
51
51
  pub struct StreamConfigurationOptions {
52
52
  /// Maximum number of unacknowledged requests that can be in flight.
53
- /// Default: 10,000
53
+ /// Default: 1,000,000
54
54
  pub max_inflight_requests: Option<u32>,
55
55
 
56
56
  /// Enable automatic stream recovery on transient failures.
@@ -103,7 +103,8 @@ pub struct TableProperties {
103
103
  pub table_name: String,
104
104
 
105
105
  /// Optional Protocol Buffer descriptor as a base64-encoded string.
106
- /// If not provided, JSON encoding will be used.
106
+ /// Omitting this does not select JSON. The stream defaults to Protocol Buffers
107
+ /// unless `record_type` is set to JSON.
107
108
  pub descriptor_proto: Option<String>,
108
109
  }
109
110
 
@@ -213,15 +214,16 @@ fn convert_js_to_record_payload(env: &Env, payload: Unknown) -> Result<RustRecor
213
214
 
214
215
  /// A stream for ingesting data into a Databricks Delta table.
215
216
  ///
216
- /// The stream manages a bidirectional gRPC connection, handles acknowledgments,
217
- /// and provides automatic recovery on transient failures.
217
+ /// The stream manages JSON or Protocol Buffer ingestion over a bidirectional
218
+ /// gRPC connection, handles acknowledgments, and provides automatic recovery
219
+ /// on transient failures.
218
220
  ///
219
221
  /// # Example
220
222
  ///
221
223
  /// ```typescript
222
224
  /// const stream = await sdk.createStream(tableProps, clientId, clientSecret, options);
223
- /// const ackPromise = await stream.ingestRecord(Buffer.from([1, 2, 3]));
224
- /// const offset = await ackPromise;
225
+ /// const offset = await stream.ingestRecordOffset(Buffer.from([1, 2, 3]));
226
+ /// await stream.flush();
225
227
  /// await stream.close();
226
228
  /// ```
227
229
  #[napi]
@@ -400,6 +402,13 @@ impl ZerobusStream {
400
402
  /// This is the recommended API for high-throughput scenarios where you want to
401
403
  /// decouple record ingestion from acknowledgment tracking.
402
404
  ///
405
+ /// **Acknowledgments:** the idiomatic flow is to ingest in a loop and then `flush()`
406
+ /// once to confirm everything queued so far. The returned offset, together with
407
+ /// `waitForOffset()`, lets you confirm a specific record when you need it (acks are
408
+ /// ordered, so the last offset confirms the whole run) — prefer `flush()` for bulk.
409
+ /// Avoid calling `waitForOffset()` after every record in a tight loop, since that
410
+ /// limits throughput to one record per round-trip.
411
+ ///
403
412
  /// # Arguments
404
413
  ///
405
414
  /// * `payload` - The record data (Buffer, string, protobuf message, or plain object)
@@ -412,11 +421,14 @@ impl ZerobusStream {
412
421
  /// # Example
413
422
  ///
414
423
  /// ```typescript
415
- /// // Promise resolves immediately with offset (before server ack)
416
- /// const offset1 = await stream.ingestRecordOffset(record1);
417
- /// const offset2 = await stream.ingestRecordOffset(record2);
418
- /// // Wait for both to be acknowledged
419
- /// await stream.waitForOffset(offset2);
424
+ /// // High-throughput pattern: ingest in a loop, wait once at the end.
425
+ /// let lastOffset: bigint | null = null;
426
+ /// for (const record of records) {
427
+ /// lastOffset = await stream.ingestRecordOffset(record); // resolves on queue, no round-trip
428
+ /// }
429
+ /// // The ack watermark is monotonic: waiting on the last offset confirms all prior records.
430
+ /// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
431
+ /// // Or simply: await stream.flush();
420
432
  /// ```
421
433
  #[napi(ts_return_type = "Promise<bigint>")]
422
434
  pub fn ingest_record_offset(&self, env: Env, payload: Unknown) -> Result<JsObject> {
@@ -454,6 +466,12 @@ impl ZerobusStream {
454
466
  /// the batch is queued, without waiting for server acknowledgment. Use
455
467
  /// `waitForOffset()` to wait for acknowledgment when needed.
456
468
  ///
469
+ /// **Acknowledgments:** the idiomatic flow is to ingest your batches in a loop and
470
+ /// then `flush()` once to confirm. The returned offset, together with `waitForOffset()`,
471
+ /// confirms a specific batch when you need it (acks are ordered, so the last offset
472
+ /// confirms the whole run) — prefer `flush()` for bulk. Avoid calling `waitForOffset()`
473
+ /// after every batch in a tight loop, since that limits throughput to one round-trip per batch.
474
+ ///
457
475
  /// # Arguments
458
476
  ///
459
477
  /// * `records` - Array of record data
@@ -466,11 +484,14 @@ impl ZerobusStream {
466
484
  /// # Example
467
485
  ///
468
486
  /// ```typescript
469
- /// // Promise resolves immediately with offset (before server ack)
470
- /// const offset = await stream.ingestRecordsOffset(batch);
471
- /// if (offset !== null) {
472
- /// await stream.waitForOffset(offset);
487
+ /// // Ingest many batches without waiting, then flush once.
488
+ /// let lastOffset = null;
489
+ /// for (const batch of batches) {
490
+ /// const offset = await stream.ingestRecordsOffset(batch); // resolves on queue
491
+ /// if (offset !== null) lastOffset = offset;
473
492
  /// }
493
+ /// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
494
+ /// // Or simply: await stream.flush();
474
495
  /// ```
475
496
  #[napi(ts_return_type = "Promise<bigint | null>")]
476
497
  pub fn ingest_records_offset(&self, env: Env, records: Vec<Unknown>) -> Result<JsObject> {
@@ -511,9 +532,12 @@ impl ZerobusStream {
511
532
 
512
533
  /// Waits for a specific offset to be acknowledged by the server.
513
534
  ///
514
- /// Use this method with `ingestRecordOffset()` and `ingestRecordsOffset()` to
515
- /// selectively wait for acknowledgments. This allows you to ingest many records
516
- /// quickly and then wait only for specific offsets when needed.
535
+ /// Use this method with `ingestRecordOffset()` and `ingestRecordsOffset()` to confirm
536
+ /// a specific record before continuing. Acks are ordered, so waiting on the LAST offset
537
+ /// confirms every prior record too you never need to wait on intermediate offsets.
538
+ /// For confirming a bulk run, `flush()` is usually simpler; reach for `waitForOffset()`
539
+ /// when one particular record must be confirmed. Avoid calling it after every record in
540
+ /// a tight loop, since that limits throughput to one record per round-trip.
517
541
  ///
518
542
  /// # Arguments
519
543
  ///
@@ -527,12 +551,12 @@ impl ZerobusStream {
527
551
  /// # Example
528
552
  ///
529
553
  /// ```typescript
530
- /// const offsets = [];
554
+ /// let lastOffset: bigint | null = null;
531
555
  /// for (const record of records) {
532
- /// offsets.push(await stream.ingestRecordOffset(record));
556
+ /// lastOffset = await stream.ingestRecordOffset(record); // no per-record wait
533
557
  /// }
534
- /// // Wait for the last offset (implies all previous are also acknowledged)
535
- /// await stream.waitForOffset(offsets[offsets.length - 1]);
558
+ /// // Wait for the last offset only (implies all previous are also acknowledged).
559
+ /// if (lastOffset !== null) await stream.waitForOffset(lastOffset);
536
560
  /// ```
537
561
  #[napi(ts_return_type = "Promise<void>")]
538
562
  pub fn wait_for_offset(&self, env: Env, offset_id: BigInt) -> Result<JsObject> {
@@ -560,6 +584,11 @@ impl ZerobusStream {
560
584
  /// This method ensures all previously ingested records have been sent to the server
561
585
  /// and acknowledged. It's useful for checkpointing or ensuring data durability.
562
586
  ///
587
+ /// This is the idiomatic way to confirm records ingested via `ingestRecordOffset()` /
588
+ /// `ingestRecordsOffset()`: ingest in a loop, then `flush()` once (for a bounded batch,
589
+ /// or periodically for a long-running stream). It resolves once everything queued so
590
+ /// far is acknowledged.
591
+ ///
563
592
  /// # Errors
564
593
  ///
565
594
  /// - Timeout errors if flush takes longer than configured timeout
@@ -646,15 +675,12 @@ impl ZerobusStream {
646
675
  ///
647
676
  /// ```typescript
648
677
  /// try {
649
- /// await stream.ingestRecords(batch1);
650
- /// await stream.ingestRecords(batch2);
678
+ /// await stream.ingestRecordsOffset(batch1);
679
+ /// await stream.ingestRecordsOffset(batch2);
680
+ /// await stream.flush();
651
681
  /// } catch (error) {
652
682
  /// const unackedBatches = await stream.getUnackedBatches();
653
- ///
654
- /// // Re-ingest with new stream
655
- /// for (const batch of unackedBatches) {
656
- /// await newStream.ingestRecords(batch);
657
- /// }
683
+ /// console.log(`Batches available for recovery: ${unackedBatches.length}`);
658
684
  /// }
659
685
  /// ```
660
686
  #[napi]
@@ -687,10 +713,10 @@ impl ZerobusStream {
687
713
  /// JavaScript headers provider callback wrapper.
688
714
  ///
689
715
  /// Allows TypeScript code to provide custom authentication headers
690
- /// by implementing a getHeaders() function.
716
+ /// by implementing a getHeadersCallback() function.
691
717
  #[napi(object)]
692
718
  pub struct JsHeadersProvider {
693
- /// JavaScript function: () => Promise<Array<[string, string]>>
719
+ /// JavaScript function: () => Array<[string, string]>
694
720
  pub get_headers_callback: JsFunction,
695
721
  }
696
722
 
@@ -721,11 +747,6 @@ impl StaticHeadersProvider {
721
747
  ));
722
748
  }
723
749
 
724
- // Add TS user agent if not provided
725
- if !map.contains_key("user-agent") {
726
- map.insert("user-agent", TS_SDK_USER_AGENT.to_string());
727
- }
728
-
729
750
  Ok(Self { headers: map })
730
751
  }
731
752
  }
@@ -810,11 +831,17 @@ impl RustHeadersProvider for TsOAuthHeadersProvider {
810
831
  let mut headers = HashMap::new();
811
832
  headers.insert("authorization", format!("Bearer {}", token));
812
833
  headers.insert("x-databricks-zerobus-table-name", self.table_name.clone());
813
- headers.insert("user-agent", TS_SDK_USER_AGENT.to_string());
814
834
  Ok(headers)
815
835
  }
816
836
  }
817
837
 
838
+ #[napi(object)]
839
+ #[derive(Default)]
840
+ pub struct ZerobusSdkOptions {
841
+ /// Identifier appended to the `user-agent` header
842
+ pub application_name: Option<String>,
843
+ }
844
+
818
845
  /// The main SDK for interacting with the Databricks Zerobus service.
819
846
  ///
820
847
  /// This is the entry point for creating ingestion streams to Delta tables.
@@ -824,7 +851,8 @@ impl RustHeadersProvider for TsOAuthHeadersProvider {
824
851
  /// ```typescript
825
852
  /// const sdk = new ZerobusSdk(
826
853
  /// "https://workspace-id.zerobus.region.cloud.databricks.com",
827
- /// "https://workspace.cloud.databricks.com"
854
+ /// "https://workspace.cloud.databricks.com",
855
+ /// { applicationName: "my-app/1.0" }
828
856
  /// );
829
857
  ///
830
858
  /// const stream = await sdk.createStream(
@@ -852,13 +880,19 @@ impl ZerobusSdk {
852
880
  /// (e.g., "https://workspace-id.zerobus.region.cloud.databricks.com")
853
881
  /// * `unity_catalog_url` - The Unity Catalog endpoint URL
854
882
  /// (e.g., "https://workspace.cloud.databricks.com")
883
+ /// * `options` - Optional SDK configuration (see `ZerobusSdkOptions`),
884
+ /// including `applicationName` for server-side attribution.
855
885
  ///
856
886
  /// # Errors
857
887
  ///
858
888
  /// - Invalid endpoint URLs
859
889
  /// - Failed to extract workspace ID from the endpoint
860
890
  #[napi(constructor)]
861
- pub fn new(zerobus_endpoint: String, unity_catalog_url: String) -> Result<Self> {
891
+ pub fn new(
892
+ zerobus_endpoint: String,
893
+ unity_catalog_url: String,
894
+ options: Option<ZerobusSdkOptions>,
895
+ ) -> Result<Self> {
862
896
  let workspace_id = zerobus_endpoint
863
897
  .strip_prefix("https://")
864
898
  .or_else(|| zerobus_endpoint.strip_prefix("http://"))
@@ -870,9 +904,17 @@ impl ZerobusSdk {
870
904
  )
871
905
  })?;
872
906
 
873
- let inner = RustZerobusSdk::builder()
907
+ let options = options.unwrap_or_default();
908
+
909
+ let builder = RustZerobusSdk::builder()
874
910
  .endpoint(&zerobus_endpoint)
875
911
  .unity_catalog_url(&unity_catalog_url)
912
+ .sdk_identifier(TS_SDK_USER_AGENT);
913
+ let builder = match options.application_name {
914
+ Some(name) => builder.application_name(name),
915
+ None => builder,
916
+ };
917
+ let inner = builder
876
918
  .build()
877
919
  .map_err(|e| Error::from_reason(format!("Failed to create SDK: {}", e)))?;
878
920
 
@@ -885,7 +927,7 @@ impl ZerobusSdk {
885
927
 
886
928
  /// Creates a new ingestion stream to a Delta table.
887
929
  ///
888
- /// This method establishes a bidirectional gRPC connection to the Zerobus service
930
+ /// This method opens a JSON or Protocol Buffer stream to the Zerobus service
889
931
  /// and prepares it for data ingestion. By default, it uses OAuth 2.0 Client Credentials
890
932
  /// authentication. For custom authentication (e.g., Personal Access Tokens), provide
891
933
  /// a custom headers_provider.
@@ -927,7 +969,7 @@ impl ZerobusSdk {
927
969
  /// "", // ignored
928
970
  /// undefined,
929
971
  /// {
930
- /// getHeadersCallback: async () => [
972
+ /// getHeadersCallback: () => [
931
973
  /// ["authorization", `Bearer ${myToken}`],
932
974
  /// ["x-databricks-zerobus-table-name", tableName]
933
975
  /// ]
@@ -947,19 +989,19 @@ impl ZerobusSdk {
947
989
  // Decode the optional protobuf descriptor up-front so we can hand it
948
990
  // to the builder's `.compiled_proto()` setter; the builder constructs
949
991
  // the (now-private) `TableProperties` itself.
950
- let descriptor_proto: Option<prost_types::DescriptorProto> =
951
- if let Some(ref desc_str) = table_properties.descriptor_proto {
952
- let bytes = base64_decode(desc_str).map_err(|e| {
953
- Error::from_reason(format!("Failed to decode descriptor: {}", e))
992
+ let descriptor_proto: Option<prost_types::DescriptorProto> = if let Some(ref desc_str) =
993
+ table_properties.descriptor_proto
994
+ {
995
+ let bytes = base64_decode(desc_str)
996
+ .map_err(|e| Error::from_reason(format!("Failed to decode descriptor: {}", e)))?;
997
+ let dp: prost_types::DescriptorProto =
998
+ prost::Message::decode(&bytes[..]).map_err(|e| {
999
+ Error::from_reason(format!("Failed to parse descriptor proto: {}", e))
954
1000
  })?;
955
- let dp: prost_types::DescriptorProto = prost::Message::decode(&bytes[..])
956
- .map_err(|e| {
957
- Error::from_reason(format!("Failed to parse descriptor proto: {}", e))
958
- })?;
959
- Some(dp)
960
- } else {
961
- None
962
- };
1001
+ Some(dp)
1002
+ } else {
1003
+ None
1004
+ };
963
1005
 
964
1006
  let opts = options.unwrap_or(StreamConfigurationOptions {
965
1007
  max_inflight_requests: None,
@@ -1076,7 +1118,8 @@ impl ZerobusSdk {
1076
1118
  ///
1077
1119
  /// # Arguments
1078
1120
  ///
1079
- /// * `stream` - The failed or closed stream to recreate
1121
+ /// * `stream` - The terminally failed stream to recreate. The TypeScript wrapper
1122
+ /// must not have been closed because `close()` releases its native handle.
1080
1123
  ///
1081
1124
  /// # Returns
1082
1125
  ///
@@ -1092,12 +1135,23 @@ impl ZerobusSdk {
1092
1135
  ///
1093
1136
  /// ```typescript
1094
1137
  /// try {
1095
- /// await stream.ingestRecords(batch);
1138
+ /// await stream.ingestRecordsOffset(batch);
1139
+ /// await stream.flush();
1096
1140
  /// } catch (error) {
1097
- /// await stream.close();
1098
- /// // Recreate stream with all unacked batches re-ingested
1099
- /// const newStream = await sdk.recreateStream(stream);
1100
- /// // Continue ingesting with newStream
1141
+ /// try {
1142
+ /// const newStream = await sdk.recreateStream(stream);
1143
+ /// try {
1144
+ /// await newStream.flush();
1145
+ /// } finally {
1146
+ /// await newStream.close();
1147
+ /// }
1148
+ /// } finally {
1149
+ /// try {
1150
+ /// await stream.close();
1151
+ /// } catch (closeError) {
1152
+ /// console.error("Failed stream released:", closeError);
1153
+ /// }
1154
+ /// }
1101
1155
  /// }
1102
1156
  /// ```
1103
1157
  #[napi]
@@ -1128,8 +1182,8 @@ fn base64_decode(input: &str) -> std::result::Result<Vec<u8>, String> {
1128
1182
  }
1129
1183
 
1130
1184
  // =============================================================================
1131
- // Arrow Flight Support (Beta)
1132
- // Enabled with feature flag: cargo build --features arrow-flight
1185
+ // Arrow Flight support
1186
+ // Behind the arrow-flight feature. Enable with: npm run build:arrow
1133
1187
  // =============================================================================
1134
1188
 
1135
1189
  #[cfg(feature = "arrow-flight")]
@@ -1144,8 +1198,6 @@ use databricks_zerobus_ingest_sdk::{
1144
1198
 
1145
1199
  /// IPC compression type for Arrow Flight streams.
1146
1200
  ///
1147
- /// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
1148
- /// may still change before reaching GA.
1149
1201
  #[cfg(feature = "arrow-flight")]
1150
1202
  #[napi]
1151
1203
  pub enum IpcCompressionType {
@@ -1157,8 +1209,6 @@ pub enum IpcCompressionType {
1157
1209
 
1158
1210
  /// Configuration options for Arrow Flight streams.
1159
1211
  ///
1160
- /// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
1161
- /// may still change before reaching GA.
1162
1212
  #[cfg(feature = "arrow-flight")]
1163
1213
  #[napi(object)]
1164
1214
  #[derive(Debug, Clone)]
@@ -1215,7 +1265,6 @@ fn map_ipc_compression(value: Option<i32>) -> Option<arrow_ipc::CompressionType>
1215
1265
 
1216
1266
  /// Arrow data type enum for schema definition.
1217
1267
  ///
1218
- /// **Beta**: Arrow Flight support is in Beta.
1219
1268
  #[cfg(feature = "arrow-flight")]
1220
1269
  #[napi]
1221
1270
  pub enum ArrowDataType {
@@ -1287,7 +1336,6 @@ fn convert_arrow_data_type(dt: i32) -> RustDataType {
1287
1336
 
1288
1337
  /// Arrow field definition for schema.
1289
1338
  ///
1290
- /// **Beta**: Arrow Flight support is in Beta.
1291
1339
  #[cfg(feature = "arrow-flight")]
1292
1340
  #[napi(object)]
1293
1341
  #[derive(Debug, Clone)]
@@ -1305,8 +1353,6 @@ pub struct ArrowField {
1305
1353
  /// Unlike `TableProperties` which uses Protocol Buffers, Arrow Flight streams
1306
1354
  /// require an Arrow schema definition.
1307
1355
  ///
1308
- /// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
1309
- /// may still change before reaching GA.
1310
1356
  #[cfg(feature = "arrow-flight")]
1311
1357
  #[napi(object)]
1312
1358
  #[derive(Debug, Clone)]
@@ -1340,9 +1386,6 @@ fn build_arrow_schema(fields: &[ArrowField]) -> Arc<RustArrowSchema> {
1340
1386
  /// This stream provides a high-performance interface for streaming Arrow data
1341
1387
  /// to Databricks Delta tables using the Arrow Flight protocol.
1342
1388
  ///
1343
- /// **Beta**: Arrow Flight support is in Beta. The API is stabilising but
1344
- /// may still change before reaching GA.
1345
- ///
1346
1389
  /// # Lifecycle
1347
1390
  ///
1348
1391
  /// 1. Create a stream via `sdk.createArrowStream()`
@@ -1438,9 +1481,7 @@ impl ZerobusArrowStream {
1438
1481
  stream_ref
1439
1482
  .ingest_ipc_batch(Bytes::from(buffer_vec))
1440
1483
  .await
1441
- .map_err(|e| {
1442
- napi::Error::from_reason(format!("Failed to ingest batch: {}", e))
1443
- })
1484
+ .map_err(|e| napi::Error::from_reason(format!("Failed to ingest batch: {}", e)))
1444
1485
  },
1445
1486
  |env, offset_id| {
1446
1487
  let global: JsGlobal = env.get_global()?;
@@ -1566,9 +1607,6 @@ impl ZerobusArrowStream {
1566
1607
  impl ZerobusSdk {
1567
1608
  /// Creates a new Arrow Flight stream to a Delta table.
1568
1609
  ///
1569
- /// **Beta**: Arrow Flight support is in Beta. The API is stabilising
1570
- /// but may still change before reaching GA.
1571
- ///
1572
1610
  /// This method establishes an Arrow Flight connection to the Zerobus service
1573
1611
  /// for high-performance columnar data ingestion.
1574
1612
  ///
@@ -1682,11 +1720,10 @@ impl ZerobusSdk {
1682
1720
 
1683
1721
  /// Recreates an Arrow stream with the same configuration and re-ingests unacknowledged batches.
1684
1722
  ///
1685
- /// **Beta**: Arrow Flight support is in Beta.
1686
- ///
1687
1723
  /// # Arguments
1688
1724
  ///
1689
- /// * `stream` - The failed or closed Arrow stream to recreate
1725
+ /// * `stream` - The terminally failed Arrow stream to recreate. The TypeScript wrapper
1726
+ /// must not have been closed because `close()` releases its native handle.
1690
1727
  ///
1691
1728
  /// # Returns
1692
1729
  ///
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Options for loading a descriptor from a FileDescriptorSet.
3
+ */
4
+ export interface LoadDescriptorOptions {
5
+ /**
6
+ * Path to the descriptor file generated by protoc.
7
+ * Example: 'schemas/my_schema_descriptor.pb'
8
+ */
9
+ descriptorPath: string;
10
+
11
+ /**
12
+ * Name of the proto file within the FileDescriptorSet.
13
+ * This should match the filename in your .proto file.
14
+ * Example: 'schemas/air_quality.proto' or 'air_quality.proto'
15
+ */
16
+ protoFileName: string;
17
+
18
+ /**
19
+ * Name of the message type to extract.
20
+ * Example: 'AirQuality'
21
+ */
22
+ messageName: string;
23
+ }
24
+
25
+ /**
26
+ * Loads a specific message's DescriptorProto from a FileDescriptorSet.
27
+ *
28
+ * @param options - Configuration specifying which message to extract
29
+ * @returns Base64-encoded DescriptorProto string for the specified message
30
+ * @throws Error if the descriptor file, proto file, or message cannot be found
31
+ */
32
+ export function loadDescriptorProto(options: LoadDescriptorOptions): string;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Utility functions for working with Protocol Buffer descriptors.
5
+ *
6
+ * These utilities help extract message descriptors from FileDescriptorSets
7
+ * generated by protoc, providing flexibility in choosing which message to use.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const descriptor = require("protobufjs/ext/descriptor");
12
+
13
+ /**
14
+ * Loads a specific message's DescriptorProto from a FileDescriptorSet.
15
+ *
16
+ * @param {object} options
17
+ * @param {string} options.descriptorPath Path to the descriptor file generated by protoc.
18
+ * @param {string} options.protoFileName Name of the proto file within the FileDescriptorSet.
19
+ * @param {string} options.messageName Name of the message type to extract.
20
+ * @returns {string} Base64-encoded DescriptorProto string for the specified message.
21
+ */
22
+ function loadDescriptorProto(options) {
23
+ const { descriptorPath, protoFileName, messageName } = options;
24
+
25
+ const descriptorBytes = fs.readFileSync(descriptorPath);
26
+ const fileDescriptorSet = descriptor.FileDescriptorSet.decode(descriptorBytes);
27
+ const fileDescriptor = fileDescriptorSet.file.find((file) =>
28
+ file.name === protoFileName ||
29
+ file.name.endsWith("/" + protoFileName)
30
+ );
31
+
32
+ if (!fileDescriptor) {
33
+ throw new Error(
34
+ `Proto file '${protoFileName}' not found in descriptor. Available files: ${
35
+ fileDescriptorSet.file.map((file) => file.name).join(", ")
36
+ }`
37
+ );
38
+ }
39
+
40
+ const messageType = fileDescriptor.messageType?.find((message) => message.name === messageName);
41
+
42
+ if (!messageType) {
43
+ const availableMessages = fileDescriptor.messageType?.map((message) => message.name).join(", ") || "none";
44
+ throw new Error(
45
+ `Message '${messageName}' not found in ${protoFileName}. Available messages: ${availableMessages}`
46
+ );
47
+ }
48
+
49
+ const descriptorProtoBytes = descriptor.DescriptorProto.encode(messageType).finish();
50
+ return Buffer.from(descriptorProtoBytes).toString("base64");
51
+ }
52
+
53
+ module.exports = {
54
+ loadDescriptorProto,
55
+ };
@@ -49,7 +49,7 @@ export interface LoadDescriptorOptions {
49
49
  *
50
50
  * @example
51
51
  * ```typescript
52
- * import { loadDescriptorProto } from '@databricks/zerobus-sdk/utils/descriptor';
52
+ * import { loadDescriptorProto } from '@databricks/zerobus-ingest-sdk/utils/descriptor.js';
53
53
  *
54
54
  * const descriptorBase64 = loadDescriptorProto({
55
55
  * descriptorPath: 'schemas/air_quality_descriptor.pb',
@@ -74,7 +74,7 @@ export function loadDescriptorProto(options: LoadDescriptorOptions): string {
74
74
 
75
75
  // Find the file descriptor matching the proto file name
76
76
  const fileDescriptor = fileDescriptorSet.file.find((f: any) =>
77
- f.name === protoFileName || f.name.endsWith('/' + protoFileName) || f.name.endsWith(protoFileName)
77
+ f.name === protoFileName || f.name.endsWith('/' + protoFileName)
78
78
  );
79
79
 
80
80
  if (!fileDescriptor) {