snowpipe-streaming 1.7.0 → 1.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snowpipe-streaming",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Snowflake Streaming Ingest SDK for Node.js",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -27,11 +27,11 @@
27
27
  "typedoc": "^0.28.19"
28
28
  },
29
29
  "optionalDependencies": {
30
- "snowpipe-streaming-linux-x64-gnu": "1.7.0",
31
- "snowpipe-streaming-linux-x64-musl": "1.7.0",
32
- "snowpipe-streaming-linux-arm64-gnu": "1.7.0",
33
- "snowpipe-streaming-linux-arm64-musl": "1.7.0",
34
- "snowpipe-streaming-darwin-arm64": "1.7.0",
35
- "snowpipe-streaming-win32-x64-msvc": "1.7.0"
30
+ "snowpipe-streaming-linux-x64-gnu": "1.7.1",
31
+ "snowpipe-streaming-linux-x64-musl": "1.7.1",
32
+ "snowpipe-streaming-linux-arm64-gnu": "1.7.1",
33
+ "snowpipe-streaming-linux-arm64-musl": "1.7.1",
34
+ "snowpipe-streaming-darwin-arm64": "1.7.1",
35
+ "snowpipe-streaming-win32-x64-msvc": "1.7.1"
36
36
  }
37
37
  }
@@ -43,6 +43,12 @@ class StreamingIngestElasticChannel {
43
43
  }
44
44
  this._native = nativeChannel;
45
45
  this._pendingFutures = pendingFutures;
46
+ // Per-channel future id counter; the wrapper allocates ids so it can register
47
+ // the promise before the row becomes ack-eligible. The single-threaded event
48
+ // loop makes the plain increment safe. The id is a JS number (a safe integer,
49
+ // exact to 2^53) marshaled to the Rust i64.
50
+ // Starts at 1; 0 is reserved as the "no future id" sentinel, matching the other SDKs.
51
+ this._nextFutureId = 1;
46
52
  }
47
53
 
48
54
  /** @returns {string} The channel name (always "ELASTIC") */
@@ -93,10 +99,10 @@ class StreamingIngestElasticChannel {
93
99
  /**
94
100
  * Append multiple rows into the elastic channel.
95
101
  *
96
- * The native call is synchronous: it buffers the rows and returns a future id,
97
- * which is registered in the shared pending map before returning to the event
98
- * loop. The Promise resolves later when the mark-complete callback fires with
99
- * the server acknowledgement (or rejects on error / channel invalidation).
102
+ * The wrapper allocates the future id and registers its promise in the shared
103
+ * pending map before the synchronous native append, so the ack can never
104
+ * precede registration. The Promise resolves later when the mark-complete
105
+ * callback fires with the server acknowledgement (or rejects on error).
100
106
  *
101
107
  * @param {Object[]} rows - Array of row objects
102
108
  * @returns {Promise<void>} Resolves when Snowflake acknowledges the batch
@@ -111,18 +117,13 @@ class StreamingIngestElasticChannel {
111
117
  // Empty arrays are validated by the Rust core, which throws a proper error.
112
118
  const buffer =
113
119
  rows.length === 0 ? Buffer.alloc(0) : serializeRowsToNdjson(rows, this.binaryInputFormat);
114
- let futureId;
115
- try {
116
- futureId = Number(this._native.appendRows(buffer, rows.length));
117
- } catch (e) {
118
- throw StreamingIngestError.from(e);
119
- }
120
120
 
121
- // Build the promise out-of-line so we can stash {resolve, reject} into the
122
- // shared map synchronously — the single-threaded event loop guarantees this
123
- // happens-before any queued threadsafe-function callback runs. Map and
124
- // resolve are then visible to mark_complete_callback.js (the JS owner of
125
- // resolution) and the invalidate-all path on channel teardown.
121
+ // Allocate the id and register the promise BEFORE the native append, so the
122
+ // row cannot become ack-eligible before its promise is in the pending map.
123
+ // Building the promise out-of-line lets us stash {resolve, reject} into the
124
+ // shared map synchronously; the single-threaded event loop guarantees this
125
+ // happens-before any queued threadsafe-function callback runs.
126
+ const futureId = this._nextFutureId++;
126
127
  let resolveFn;
127
128
  let rejectFn;
128
129
  const promise = new Promise((resolve, reject) => {
@@ -132,10 +133,7 @@ class StreamingIngestElasticChannel {
132
133
  const previous = this._pendingFutures.get(futureId);
133
134
  this._pendingFutures.set(futureId, { resolve: resolveFn, reject: rejectFn });
134
135
  if (previous !== undefined) {
135
- // Mirrors Java/Python: a duplicate future id from Rust means the SDK
136
- // and the core have drifted, which is fatal. Reject the displaced
137
- // promise, invalidate the channel, and throw to the caller so the
138
- // new promise we just stashed isn't left hanging either.
136
+ // Monotonic per-channel ids make this a should-never-happen invariant.
139
137
  const errMsg = `Duplicate future_id=${futureId} in pendingFutures`;
140
138
  const err = new StreamingIngestError("Fatal", errMsg, 500, "Internal Server Error");
141
139
  previous.reject(err);
@@ -147,6 +145,14 @@ class StreamingIngestElasticChannel {
147
145
  }
148
146
  throw err;
149
147
  }
148
+
149
+ try {
150
+ this._native.appendRows(buffer, rows.length, futureId);
151
+ } catch (e) {
152
+ // Never enqueued (not ack-eligible), so no ack can arrive; drop the promise.
153
+ this._pendingFutures.delete(futureId);
154
+ throw StreamingIngestError.from(e);
155
+ }
150
156
  return promise;
151
157
  }
152
158
 
@@ -162,6 +168,15 @@ class StreamingIngestElasticChannel {
162
168
  }
163
169
  }
164
170
 
171
+ /**
172
+ * Wait for this channel to flush.
173
+ * @param {{timeoutMs?: number}} [options]
174
+ * @returns {Promise<void>}
175
+ */
176
+ async waitForFlush(options) {
177
+ return callFFI(() => this._native.waitForFlush(options?.timeoutMs));
178
+ }
179
+
165
180
  /**
166
181
  * Get the current status of this channel.
167
182
  * @returns {Promise<ChannelStatus>}
package/src/index.d.ts CHANGED
@@ -380,6 +380,18 @@ export class StreamingIngestElasticChannel {
380
380
  */
381
381
  initiateFlush(): void;
382
382
 
383
+ /**
384
+ * Wait for all buffered data in this channel to be flushed to Snowflake.
385
+ *
386
+ * @param options - Wait options
387
+ * @throws StreamingIngestError If waiting for the flush fails
388
+ * @throws Error If the timeout is reached
389
+ */
390
+ waitForFlush(options?: {
391
+ /** Optional timeout in milliseconds. */
392
+ timeoutMs?: number;
393
+ }): Promise<void>;
394
+
383
395
  /**
384
396
  * Get the current status of this channel.
385
397
  *