@pipe0/client 1.0.0 → 1.0.2

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/dist/pipe0.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { CapacityTracker } from "./capacity-tracker.mjs";
1
2
  import createFetchClient from "openapi-fetch";
2
3
 
3
4
  //#region src/pipe0.ts
@@ -17,6 +18,17 @@ var Pipe0AbortError = class extends Error {
17
18
  this.runId = runId;
18
19
  }
19
20
  };
21
+ /**
22
+ * The server reports the run as `canceled` (via `pipes.cancel` /
23
+ * `searches.cancel` or another client). Subclasses {@link Pipe0AbortError} so
24
+ * existing `instanceof Pipe0AbortError` handling keeps working.
25
+ */
26
+ var Pipe0CanceledError = class extends Pipe0AbortError {
27
+ constructor(message, runId) {
28
+ super(message, runId);
29
+ this.name = "Pipe0CanceledError";
30
+ }
31
+ };
20
32
  var Pipe0BatchError = class extends Error {
21
33
  constructor(message, errors, successfulBatches) {
22
34
  super(message);
@@ -36,11 +48,53 @@ var Pipe0TaskError = class extends Error {
36
48
  }
37
49
  };
38
50
  var Pipe0ServerError = class extends Error {
39
- constructor(message) {
51
+ /** HTTP status of the error response, when known. */
52
+ status;
53
+ /** RFC 7807 problem `type` discriminator (e.g. "queue-limit-exceeded", "org-rate-limited"). */
54
+ problemType;
55
+ constructor(message, options = {}) {
40
56
  super(message);
41
57
  this.name = "Pipe0ServerError";
58
+ this.status = options.status;
59
+ this.problemType = options.problemType;
60
+ }
61
+ };
62
+ /**
63
+ * Thrown only after the SDK's automatic 429 handling exhausted its wait
64
+ * budget (`rateLimitMaxWaitMs` / `queueFullMaxWaitMs`), or immediately when
65
+ * `autoRetry429` is disabled the error surfaces as a plain
66
+ * {@link Pipe0ServerError} instead. Subclasses Pipe0ServerError so existing
67
+ * handling keeps working.
68
+ */
69
+ var Pipe0RateLimitError = class extends Pipe0ServerError {
70
+ /** The delay the SDK would have waited next, in ms. */
71
+ retryAfterMs;
72
+ /** Attempts made before giving up. */
73
+ attempts;
74
+ constructor(message, options = {}) {
75
+ super(message, {
76
+ status: 429,
77
+ problemType: options.problemType
78
+ });
79
+ this.name = "Pipe0RateLimitError";
80
+ this.retryAfterMs = options.retryAfterMs;
81
+ this.attempts = options.attempts ?? 1;
42
82
  }
43
83
  };
84
+ /** The run-creating endpoints — the only responses that carry capacity headers. */
85
+ const CAPACITY_HEADER_PATHS = new Set([
86
+ "/v1/pipes/run",
87
+ "/v1/pipes/run/sync",
88
+ "/v1/search/run",
89
+ "/v1/search/run/sync",
90
+ "/v1/searches/run",
91
+ "/v1/searches/run/sync",
92
+ "/v1/pipes/cancel/{run_id}",
93
+ "/v1/search/cancel/{run_id}"
94
+ ]);
95
+ function extractProblemType(error) {
96
+ if (error !== null && typeof error === "object" && "type" in error && typeof error.type === "string") return error.type;
97
+ }
44
98
  var Pipe0 = class {
45
99
  client;
46
100
  apiKey;
@@ -49,6 +103,15 @@ var Pipe0 = class {
49
103
  maxPollingIntervalMs;
50
104
  defaultBatchSize;
51
105
  maxConcurrentBatches;
106
+ autoRetry429;
107
+ retryAfterCapMs;
108
+ rateLimitMaxWaitMs;
109
+ queueFullMaxWaitMs;
110
+ defaultCancelOnAbort;
111
+ defaultPriorityFloor;
112
+ pacingProbeMs;
113
+ onCapacity;
114
+ tracker = new CapacityTracker();
52
115
  constructor(options = {}) {
53
116
  const envApiKey = typeof process !== "undefined" ? process.env?.PIPE0_API_KEY : void 0;
54
117
  this.apiKey = options.apiKey ?? envApiKey;
@@ -57,13 +120,92 @@ var Pipe0 = class {
57
120
  this.maxPollingIntervalMs = options.maxPollingIntervalMs ?? this.minPollingIntervalMs * 3;
58
121
  this.defaultBatchSize = options.defaultBatchSize ?? 100;
59
122
  this.maxConcurrentBatches = options.maxConcurrentBatches ?? 5;
123
+ this.autoRetry429 = options.autoRetry429 ?? true;
124
+ this.retryAfterCapMs = options.retryAfterCapMs ?? 3e4;
125
+ this.rateLimitMaxWaitMs = options.rateLimitMaxWaitMs ?? 12e4;
126
+ this.queueFullMaxWaitMs = options.queueFullMaxWaitMs ?? 9e5;
127
+ this.defaultCancelOnAbort = options.cancelOnAbort ?? true;
128
+ this.defaultPriorityFloor = options.priorityFloor ?? 2;
129
+ this.pacingProbeMs = options.pacingProbeMs ?? 15e3;
130
+ this.onCapacity = options.onCapacity;
60
131
  const headers = {};
61
132
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
62
133
  this.client = createFetchClient({
63
134
  baseUrl: options.baseUrl ?? "https://api.pipe0.com",
64
135
  credentials: options.credentials ? options.credentials : options.baseUrl ? "same-origin" : "include",
65
- headers
136
+ headers,
137
+ fetch: options.fetch
66
138
  });
139
+ this.client.use({ onResponse: ({ schemaPath, response }) => {
140
+ if (!CAPACITY_HEADER_PATHS.has(schemaPath)) return void 0;
141
+ const snapshot = this.tracker.observeHeaders(response.headers);
142
+ if (snapshot && this.onCapacity) try {
143
+ this.onCapacity(snapshot);
144
+ } catch {}
145
+ } });
146
+ }
147
+ /**
148
+ * The SDK's current estimate of your organization's queue capacity, from
149
+ * the most recent run-creating response's headers adjusted by runs the SDK
150
+ * has since observed completing. Null until the first create.
151
+ */
152
+ get capacity() {
153
+ return this.tracker.snapshot();
154
+ }
155
+ /**
156
+ * The single request funnel: every API call goes through here so 429
157
+ * handling and capacity bookkeeping live in exactly one place.
158
+ *
159
+ * - `org-rate-limited` (request bucket, has Retry-After): sleep and retry,
160
+ * budget `rateLimitMaxWaitMs`.
161
+ * - `queue-limit-exceeded` (queued-record hard ceiling, no Retry-After):
162
+ * exponential backoff retry, budget `queueFullMaxWaitMs` — re-issuing the
163
+ * create is safe because the ceiling rejects before anything is queued.
164
+ * - Anything else throws {@link Pipe0ServerError} with `status` +
165
+ * `problemType` attached.
166
+ */
167
+ async request(fn, opts = {}) {
168
+ const retryEnabled = opts.retry ?? this.autoRetry429;
169
+ const startedAt = Date.now();
170
+ let attempts = 0;
171
+ let queueDelayMs = 5e3;
172
+ while (true) {
173
+ attempts += 1;
174
+ if (opts.capacity) this.tracker.onCreateIssued(opts.capacity.records);
175
+ let result;
176
+ try {
177
+ result = await fn();
178
+ } finally {
179
+ if (opts.capacity) this.tracker.onCreateSettled(opts.capacity.records);
180
+ }
181
+ if (!result.error) return result.data;
182
+ const status = result.response?.status;
183
+ const problemType = extractProblemType(result.error);
184
+ if (status !== 429 || !retryEnabled) throw new Pipe0ServerError(JSON.stringify(result.error), {
185
+ status,
186
+ problemType
187
+ });
188
+ const isQueueFull = problemType === "queue-limit-exceeded";
189
+ let delayMs;
190
+ let budgetMs;
191
+ if (isQueueFull) {
192
+ delayMs = queueDelayMs;
193
+ queueDelayMs = Math.min(queueDelayMs * 2, 6e4);
194
+ budgetMs = this.queueFullMaxWaitMs;
195
+ } else {
196
+ const retryAfterSeconds = Number(result.response.headers.get("Retry-After"));
197
+ const baseMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : 5e3;
198
+ delayMs = Math.min(Math.max(baseMs, 1e3), this.retryAfterCapMs);
199
+ budgetMs = this.rateLimitMaxWaitMs;
200
+ }
201
+ delayMs = Math.round(delayMs * (1 + Math.random() * .25));
202
+ if (Date.now() - startedAt + delayMs > budgetMs) throw new Pipe0RateLimitError(`Request still rate limited (${problemType ?? "429"}) after ${attempts} attempt(s) over ${Date.now() - startedAt}ms`, {
203
+ problemType,
204
+ retryAfterMs: delayMs,
205
+ attempts
206
+ });
207
+ await this.sleep(delayMs, opts.signal);
208
+ }
67
209
  }
68
210
  /**
69
211
  * Centralized logic to poll until a task completes, fails, or times out.
@@ -73,107 +215,184 @@ var Pipe0 = class {
73
215
  const timeout = this.pollingTimeoutMs;
74
216
  const initialInterval = this.minPollingIntervalMs;
75
217
  const maxInterval = this.maxPollingIntervalMs;
218
+ const cancelOnAbort = options.cancelOnAbort ?? this.defaultCancelOnAbort;
76
219
  const startTime = Date.now();
77
220
  let currentInterval = initialInterval;
78
- while (true) {
79
- if (Date.now() - startTime >= timeout) throw new Pipe0TimeoutError(`${taskType} polling timed out after ${timeout}ms`, resolvedRunId);
80
- if (options.signal?.aborted) throw new Pipe0AbortError(`${taskType} polling was aborted`, resolvedRunId);
81
- const polledResponse = await checkFn(resolvedRunId);
82
- if (options.onPoll) await options.onPoll(polledResponse);
83
- if (polledResponse.status === "completed") return polledResponse;
84
- if (polledResponse.status === "skipped") return polledResponse;
85
- if (polledResponse.status === "canceled") throw new Pipe0AbortError(`${taskType} run was canceled`, resolvedRunId);
86
- if (polledResponse.status === "failed") throw new Pipe0TaskError(`${taskType} failed with status: ${polledResponse.status}`, polledResponse);
87
- await this.sleep(currentInterval, options.signal);
88
- currentInterval = Math.min(currentInterval * 1.1, maxInterval);
221
+ try {
222
+ while (true) {
223
+ if (Date.now() - startTime >= timeout) throw new Pipe0TimeoutError(`${taskType} polling timed out after ${timeout}ms`, resolvedRunId);
224
+ if (options.signal?.aborted) throw new Pipe0AbortError(`${taskType} polling was aborted`, resolvedRunId);
225
+ const polledResponse = await checkFn(resolvedRunId);
226
+ if (options.onPoll) await options.onPoll(polledResponse);
227
+ if (polledResponse.status === "completed") {
228
+ this.tracker.creditTerminal(resolvedRunId);
229
+ return polledResponse;
230
+ }
231
+ if (polledResponse.status === "skipped") {
232
+ this.tracker.creditTerminal(resolvedRunId);
233
+ return polledResponse;
234
+ }
235
+ if (polledResponse.status === "canceled") {
236
+ this.tracker.creditTerminal(resolvedRunId);
237
+ throw new Pipe0CanceledError(`${taskType} run was canceled`, resolvedRunId);
238
+ }
239
+ if (polledResponse.status === "failed") {
240
+ this.tracker.creditTerminal(resolvedRunId);
241
+ throw new Pipe0TaskError(`${taskType} failed with status: ${polledResponse.status}`, polledResponse);
242
+ }
243
+ await this.sleep(currentInterval, options.signal);
244
+ currentInterval = Math.min(currentInterval * 1.1, maxInterval);
245
+ }
246
+ } catch (error) {
247
+ if (cancelOnAbort && options.cancelFn && options.signal?.aborted && !(error instanceof Pipe0CanceledError)) options.cancelFn(resolvedRunId).catch(() => {});
248
+ throw error;
89
249
  }
90
250
  }
91
251
  pipes = {
92
252
  pipe: async (payload, options = {}) => {
93
- return this.pipes.waitUntilComplete(await this.pipes.create(payload), options);
253
+ return this.pipes.waitUntilComplete(await this.pipes.create(payload, options), options);
254
+ },
255
+ create: async (payload, options = {}) => {
256
+ const records = Array.isArray(payload.input) ? payload.input.length : 1;
257
+ const data = await this.request(() => this.client.POST("/v1/pipes/run", { body: payload }), {
258
+ signal: options.signal,
259
+ capacity: { records }
260
+ });
261
+ this.tracker.registerRun(data.id, records);
262
+ return data.id;
94
263
  },
95
- create: async (payload) => {
96
- const response = await this.client.POST("/v1/pipes/run", { body: payload });
97
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
98
- return response.data.id;
264
+ check: async (runId, options = {}) => {
265
+ return this.request(() => this.client.GET("/v1/pipes/check/{run_id}", {
266
+ params: { path: { run_id: runId } },
267
+ signal: options.signal
268
+ }), { signal: options.signal });
99
269
  },
100
- check: async (runId) => {
101
- const response = await this.client.GET("/v1/pipes/check/{run_id}", { params: { path: { run_id: runId } } });
102
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
103
- return response.data;
270
+ cancel: async (runId, options = {}) => {
271
+ const data = await this.request(() => this.client.POST("/v1/pipes/cancel/{run_id}", {
272
+ params: { path: { run_id: runId } },
273
+ signal: options.signal
274
+ }), { signal: options.signal });
275
+ this.tracker.creditTerminal(runId);
276
+ return data;
104
277
  },
105
278
  waitUntilComplete: async (runId, options = {}) => {
106
- return this._pollUntilComplete(runId, this.pipes.check, "Pipe", options);
279
+ return this._pollUntilComplete(runId, (id) => this.pipes.check(id, { signal: options.signal }), "Pipe", {
280
+ ...options,
281
+ cancelFn: (id) => this.pipes.cancel(id)
282
+ });
107
283
  },
108
- pipeInBatches: async (payload, { stopOnError = true, ...options } = {}) => {
284
+ pipeInBatches: async (payload, { stopOnError = true, priorityFloor, cancelOnAbort, ...options } = {}) => {
109
285
  const batchSize = this.defaultBatchSize;
110
286
  const maxConcurrency = this.maxConcurrentBatches;
287
+ const floor = priorityFloor ?? this.defaultPriorityFloor;
288
+ const doCancelOnAbort = cancelOnAbort ?? this.defaultCancelOnAbort;
111
289
  if (!Array.isArray(payload.input)) throw new Error("payload.input must be an array for batched operations");
112
290
  const inputArray = payload.input;
113
- const totalItems = inputArray.length;
114
291
  const batches = [];
115
- for (let i = 0; i < totalItems; i += batchSize) {
116
- const batchInput = inputArray.slice(i, i + batchSize);
117
- batches.push({
118
- ...payload,
119
- input: batchInput
120
- });
121
- }
122
- const resultsArr = [];
292
+ for (let i = 0; i < inputArray.length; i += batchSize) batches.push({
293
+ ...payload,
294
+ input: inputArray.slice(i, i + batchSize)
295
+ });
296
+ const results = new Array(batches.length);
123
297
  const errors = [];
124
- for (let i = 0; i < batches.length; i += maxConcurrency) {
125
- if (options.signal?.aborted) throw new Pipe0AbortError("Batch operation was aborted");
126
- const batchPromises = batches.slice(i, i + maxConcurrency).map(async (batch, groupIndex) => {
127
- const batchIndex = i + groupIndex;
128
- try {
129
- const runId = await this.pipes.create(batch);
130
- const result = await this.pipes.waitUntilComplete(runId, {
131
- signal: options.signal,
132
- onPoll: options.onPoll ? (response) => options.onPoll(batchIndex, response) : void 0
133
- });
134
- if (options.onBatchComplete) options.onBatchComplete(batchIndex, result);
135
- return {
136
- batchIndex,
137
- result
138
- };
139
- } catch (error) {
140
- const err = error instanceof Error ? error : new Error(String(error));
141
- errors.push({
142
- batchIndex,
143
- error: err
144
- });
145
- if (stopOnError) throw new Pipe0BatchError(`Batch ${batchIndex} failed and stopOnError is enabled`, errors, resultsArr);
146
- return {
147
- batchIndex,
148
- result: null
149
- };
298
+ const inFlight = /* @__PURE__ */ new Set();
299
+ /** batchIndex → runId while the run is still cancelable. */
300
+ const pendingRuns = /* @__PURE__ */ new Map();
301
+ let next = 0;
302
+ let stopped = false;
303
+ const sweepCancel = () => {
304
+ if (!doCancelOnAbort) return;
305
+ for (const runId of pendingRuns.values()) this.pipes.cancel(runId).catch(() => {});
306
+ pendingRuns.clear();
307
+ };
308
+ const runBatch = async (batchIndex) => {
309
+ try {
310
+ const runId = await this.pipes.create(batches[batchIndex], { signal: options.signal });
311
+ pendingRuns.set(batchIndex, runId);
312
+ const result = await this.pipes.waitUntilComplete(runId, {
313
+ signal: options.signal,
314
+ cancelOnAbort: false,
315
+ onPoll: options.onPoll ? (response) => options.onPoll(batchIndex, response) : void 0
316
+ });
317
+ pendingRuns.delete(batchIndex);
318
+ results[batchIndex] = result;
319
+ if (options.onBatchComplete) options.onBatchComplete(batchIndex, result);
320
+ } catch (error) {
321
+ if (error instanceof Pipe0AbortError && options.signal?.aborted) return;
322
+ pendingRuns.delete(batchIndex);
323
+ errors.push({
324
+ batchIndex,
325
+ error: error instanceof Error ? error : new Error(String(error))
326
+ });
327
+ if (stopOnError) stopped = true;
328
+ }
329
+ };
330
+ const submit = (batchIndex) => {
331
+ const p = runBatch(batchIndex).finally(() => inFlight.delete(p));
332
+ inFlight.add(p);
333
+ };
334
+ try {
335
+ while (next < batches.length && !stopped || inFlight.size > 0) {
336
+ if (options.signal?.aborted) throw new Pipe0AbortError("Batch operation was aborted");
337
+ if (!stopped && next < batches.length && inFlight.size < maxConcurrency) {
338
+ const records = batches[next].input.length;
339
+ const estimate = this.tracker.estimate(floor);
340
+ if (estimate === null || estimate >= records) {
341
+ submit(next++);
342
+ continue;
343
+ }
344
+ if (inFlight.size === 0) {
345
+ await this.sleep(this.pacingProbeMs, options.signal);
346
+ if (options.signal?.aborted) throw new Pipe0AbortError("Batch operation was aborted");
347
+ submit(next++);
348
+ continue;
349
+ }
150
350
  }
151
- });
152
- const batchResults = await Promise.all(batchPromises);
153
- for (const { result } of batchResults) if (result) resultsArr.push(result);
351
+ if (inFlight.size > 0) await Promise.race(inFlight);
352
+ }
353
+ } catch (error) {
354
+ sweepCancel();
355
+ throw error;
154
356
  }
155
- if (errors.length > 0) throw new Pipe0BatchError(`${errors.length} batch(es) failed out of ${batches.length} total`, errors, resultsArr);
156
- return resultsArr;
357
+ const compact = results.filter((r) => r !== void 0);
358
+ if (stopped) throw new Pipe0BatchError(`Batch ${errors[0]?.batchIndex} failed and stopOnError is enabled`, errors, compact);
359
+ if (errors.length > 0) throw new Pipe0BatchError(`${errors.length} batch(es) failed out of ${batches.length} total`, errors, compact);
360
+ return compact;
157
361
  }
158
362
  };
159
363
  searches = {
160
364
  search: async (payload, options = {}) => {
161
- return this.searches.waitUntilComplete(await this.searches.create(payload), options);
365
+ return this.searches.waitUntilComplete(await this.searches.create(payload, options), options);
366
+ },
367
+ create: async (payload, options = {}) => {
368
+ const data = await this.request(() => this.client.POST("/v1/search/run", { body: payload }), {
369
+ signal: options.signal,
370
+ capacity: { records: 1 }
371
+ });
372
+ this.tracker.registerRun(data.id, 1);
373
+ return data.id;
162
374
  },
163
- create: async (payload) => {
164
- const response = await this.client.POST("/v1/search/run", { body: payload });
165
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
166
- return response.data.id;
375
+ check: async (runId, options = {}) => {
376
+ return this.request(() => this.client.GET("/v1/search/check/{run_id}", {
377
+ params: { path: { run_id: runId } },
378
+ signal: options.signal
379
+ }), { signal: options.signal });
167
380
  },
168
- check: async (runId) => {
169
- const response = await this.client.GET("/v1/search/check/{run_id}", { params: { path: { run_id: runId } } });
170
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
171
- return response.data;
381
+ cancel: async (runId, options = {}) => {
382
+ const data = await this.request(() => this.client.POST("/v1/search/cancel/{run_id}", {
383
+ params: { path: { run_id: runId } },
384
+ signal: options.signal
385
+ }), { signal: options.signal });
386
+ this.tracker.creditTerminal(runId);
387
+ return data;
172
388
  },
173
389
  waitUntilComplete: async (runId, options = {}) => {
174
- return this._pollUntilComplete(runId, this.searches.check, "Search", options);
390
+ return this._pollUntilComplete(runId, (id) => this.searches.check(id, { signal: options.signal }), "Search", {
391
+ ...options,
392
+ cancelFn: (id) => this.searches.cancel(id)
393
+ });
175
394
  },
176
- searchAll: async ({ searches, config, dedupeBy, maxPages = 1, normalize = true, maxConcurrency = this.maxConcurrentBatches, signal, stopOnError = true }) => {
395
+ searchAll: async ({ searches, config, dedupeBy, maxPages = 1, normalize = true, maxConcurrency = this.maxConcurrentBatches, signal, stopOnError = true, cancelOnAbort }) => {
177
396
  const errors = [];
178
397
  const merged = [];
179
398
  const runItem = async (item) => {
@@ -185,7 +404,10 @@ var Pipe0 = class {
185
404
  const res = await this.searches.search({
186
405
  config,
187
406
  search
188
- }, { signal });
407
+ }, {
408
+ signal,
409
+ cancelOnAbort
410
+ });
189
411
  rows.push(...res.results);
190
412
  const next = res.next_page;
191
413
  if (!next) break;
@@ -220,23 +442,22 @@ var Pipe0 = class {
220
442
  };
221
443
  sheets = { effects: {
222
444
  run: async (sheetId, payload) => {
223
- const response = await this.client.POST("/v1/sheets/{sheet_id}/effects/run", {
445
+ return this.request(() => this.client.POST("/v1/sheets/{sheet_id}/effects/run", {
224
446
  params: { path: { sheet_id: sheetId } },
225
447
  body: payload
226
- });
227
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
228
- return response.data;
448
+ }));
229
449
  },
230
- check: async (sheetId, runId) => {
231
- const response = await this.client.GET("/v1/sheets/{sheet_id}/effects/check/{run_id}", { params: { path: {
232
- sheet_id: sheetId,
233
- run_id: runId
234
- } } });
235
- if (response.error) throw new Pipe0ServerError(JSON.stringify(response.error));
236
- return response.data;
450
+ check: async (sheetId, runId, options = {}) => {
451
+ return this.request(() => this.client.GET("/v1/sheets/{sheet_id}/effects/check/{run_id}", {
452
+ params: { path: {
453
+ sheet_id: sheetId,
454
+ run_id: runId
455
+ } },
456
+ signal: options.signal
457
+ }), { signal: options.signal });
237
458
  },
238
459
  waitUntilComplete: async (sheetId, runId, options = {}) => {
239
- return this._pollUntilComplete(runId, (id) => this.sheets.effects.check(sheetId, id), "Effects", options);
460
+ return this._pollUntilComplete(runId, (id) => this.sheets.effects.check(sheetId, id, { signal: options.signal }), "Effects", options);
240
461
  },
241
462
  runUntilComplete: async (sheetId, payload, options = {}) => {
242
463
  const { id } = await this.sheets.effects.run(sheetId, payload);
@@ -284,7 +505,7 @@ function dedupeResults(rows, fields) {
284
505
  out.push(row);
285
506
  continue;
286
507
  }
287
- const key = parts.join("\0");
508
+ const key = parts.join(" ");
288
509
  if (seen.has(key)) continue;
289
510
  seen.add(key);
290
511
  out.push(row);
@@ -308,5 +529,5 @@ function normalizeResults(rows) {
308
529
  }
309
530
 
310
531
  //#endregion
311
- export { Pipe0, Pipe0AbortError, Pipe0BatchError, Pipe0ServerError, Pipe0TaskError, Pipe0TimeoutError };
532
+ export { Pipe0, Pipe0AbortError, Pipe0BatchError, Pipe0CanceledError, Pipe0RateLimitError, Pipe0ServerError, Pipe0TaskError, Pipe0TimeoutError };
312
533
  //# sourceMappingURL=pipe0.mjs.map