deepline 0.1.294 → 0.1.296

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.
@@ -0,0 +1,662 @@
1
+ type RetryableRuntimeSheetWriteError = {
2
+ retryAfterMs: number;
3
+ };
4
+
5
+ type Deferred = {
6
+ promise: Promise<void>;
7
+ resolve: () => void;
8
+ reject: (error: unknown) => void;
9
+ };
10
+
11
+ type PendingTerminal<Row> = {
12
+ kind: 'terminal';
13
+ key: string;
14
+ value: Row;
15
+ bytes: number;
16
+ committed: Deferred[];
17
+ };
18
+
19
+ type PendingCheckpoint<Update> = {
20
+ kind: 'checkpoint';
21
+ key: string;
22
+ value: Update;
23
+ bytes: number;
24
+ };
25
+
26
+ type PendingWrite<Row, Update> =
27
+ | PendingTerminal<Row>
28
+ | PendingCheckpoint<Update>;
29
+
30
+ type BlockedTerminal<Row> = {
31
+ kind: 'terminal';
32
+ key: string;
33
+ value: Row;
34
+ bytes: number;
35
+ admitted: Deferred;
36
+ committed: Deferred;
37
+ };
38
+
39
+ type BlockedCheckpoint<Update> = {
40
+ kind: 'checkpoint';
41
+ key: string;
42
+ value: Update;
43
+ bytes: number;
44
+ admitted: Deferred;
45
+ };
46
+
47
+ type BlockedWrite<Row, Update> =
48
+ | BlockedTerminal<Row>
49
+ | BlockedCheckpoint<Update>;
50
+
51
+ export type RuntimeSheetRowWriterBatch<Row, Update> =
52
+ | {
53
+ kind: 'terminal';
54
+ rows: readonly Row[];
55
+ }
56
+ | {
57
+ kind: 'checkpoint';
58
+ updates: readonly Update[];
59
+ };
60
+
61
+ export type RuntimeSheetRowWriterBatchResult = {
62
+ committed?: number;
63
+ staleKeys?: readonly string[];
64
+ };
65
+
66
+ export type RuntimeSheetRowWriterSummary = {
67
+ batches: number;
68
+ checkpointRows: number;
69
+ staleKeys: string[];
70
+ terminalRows: number;
71
+ };
72
+
73
+ export type RuntimeSheetRowSettlement = {
74
+ /**
75
+ * Resolves once the bounded live buffer owns this row. Awaiting this promise
76
+ * is the writer's admission backpressure seam; it does not wait for Neon.
77
+ */
78
+ admitted: Promise<void>;
79
+ /** Resolves only after the batch containing the row commits durably. */
80
+ committed: Promise<void>;
81
+ };
82
+
83
+ export type RuntimeSheetRowWriterOptions<Row, Update> = {
84
+ terminalKey: (row: Row) => string;
85
+ checkpointKey: (update: Update) => string;
86
+ mergeCheckpointUpdates: (current: Update, incoming: Update) => Update;
87
+ writeBatch: (
88
+ batch: RuntimeSheetRowWriterBatch<Row, Update>,
89
+ options: { signal: AbortSignal },
90
+ ) => Promise<RuntimeSheetRowWriterBatchResult | void>;
91
+ estimateTerminalBytes?: (row: Row) => number;
92
+ estimateCheckpointBytes?: (update: Update) => number;
93
+ maxBatchRows?: number;
94
+ maxBatchBytes?: number;
95
+ maxBufferedBytes?: number;
96
+ maxFlushMs?: number;
97
+ classifyRetryableError?: (
98
+ error: unknown,
99
+ ) => RetryableRuntimeSheetWriteError | null;
100
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
101
+ onFailure?: (error: unknown) => void;
102
+ };
103
+
104
+ export class RuntimeSheetRowWriterClosedError extends Error {
105
+ constructor() {
106
+ super('Runtime Sheet row writer is closed.');
107
+ this.name = 'RuntimeSheetRowWriterClosedError';
108
+ }
109
+ }
110
+
111
+ export class RuntimeSheetRowWriterBufferLimitError extends Error {
112
+ constructor(
113
+ readonly inputBytes: number,
114
+ readonly limitBytes: number,
115
+ ) {
116
+ super(
117
+ `Runtime Sheet row write is ${inputBytes} bytes, exceeding the ` +
118
+ `${limitBytes}-byte writer limit.`,
119
+ );
120
+ this.name = 'RuntimeSheetRowWriterBufferLimitError';
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Per-map durable row writer.
126
+ *
127
+ * This Module has one live, keyed accumulator and at most one active Adapter
128
+ * call. A timer, row limit, byte limit, checkpoint, or finish may start the
129
+ * pump. Arrivals during an active write remain coalescible in the live
130
+ * accumulator instead of becoming a chain of frozen future batches.
131
+ */
132
+ export class RuntimeSheetRowWriter<Row, Update> {
133
+ readonly #terminalKey: (row: Row) => string;
134
+ readonly #checkpointKey: (update: Update) => string;
135
+ readonly #mergeCheckpointUpdates: (
136
+ current: Update,
137
+ incoming: Update,
138
+ ) => Update;
139
+ readonly #writeBatch: RuntimeSheetRowWriterOptions<Row, Update>['writeBatch'];
140
+ readonly #estimateTerminalBytes: (row: Row) => number;
141
+ readonly #estimateCheckpointBytes: (update: Update) => number;
142
+ readonly #maxBatchRows: number;
143
+ readonly #maxBatchBytes: number;
144
+ readonly #maxBufferedBytes: number;
145
+ readonly #maxFlushMs: number;
146
+ readonly #classifyRetryableError: NonNullable<
147
+ RuntimeSheetRowWriterOptions<Row, Update>['classifyRetryableError']
148
+ >;
149
+ readonly #sleep: NonNullable<
150
+ RuntimeSheetRowWriterOptions<Row, Update>['sleep']
151
+ >;
152
+ readonly #onFailure:
153
+ | RuntimeSheetRowWriterOptions<Row, Update>['onFailure']
154
+ | undefined;
155
+ readonly #live = new Map<string, PendingWrite<Row, Update>>();
156
+ readonly #blocked: Array<BlockedWrite<Row, Update>> = [];
157
+ readonly #terminalAcceptedKeys = new Set<string>();
158
+ readonly #staleKeys = new Set<string>();
159
+ readonly #controller = new AbortController();
160
+
161
+ #active: PendingWrite<Row, Update>[] | null = null;
162
+ #bufferedBytes = 0;
163
+ #liveBytes = 0;
164
+ #timer: ReturnType<typeof setTimeout> | null = null;
165
+ #pump: Promise<void> | null = null;
166
+ #checkpointPromise: Promise<RuntimeSheetRowWriterSummary> | null = null;
167
+ #finishPromise: Promise<RuntimeSheetRowWriterSummary> | null = null;
168
+ #stateWaiters: Deferred[] = [];
169
+ #failure: unknown = null;
170
+ #closed = false;
171
+ #batches = 0;
172
+ #checkpointRows = 0;
173
+ #terminalRows = 0;
174
+
175
+ constructor(options: RuntimeSheetRowWriterOptions<Row, Update>) {
176
+ this.#terminalKey = options.terminalKey;
177
+ this.#checkpointKey = options.checkpointKey;
178
+ this.#mergeCheckpointUpdates = options.mergeCheckpointUpdates;
179
+ this.#writeBatch = options.writeBatch;
180
+ this.#estimateTerminalBytes =
181
+ options.estimateTerminalBytes ?? estimateJsonBytes;
182
+ this.#estimateCheckpointBytes =
183
+ options.estimateCheckpointBytes ?? estimateJsonBytes;
184
+ this.#maxBatchRows = positiveInteger(options.maxBatchRows, 100);
185
+ this.#maxBufferedBytes = positiveInteger(
186
+ options.maxBufferedBytes,
187
+ 32 * 1024 * 1024,
188
+ );
189
+ this.#maxBatchBytes = Math.min(
190
+ positiveInteger(options.maxBatchBytes, 1024 * 1024),
191
+ this.#maxBufferedBytes,
192
+ );
193
+ this.#maxFlushMs = Math.max(0, Math.floor(options.maxFlushMs ?? 500));
194
+ this.#classifyRetryableError =
195
+ options.classifyRetryableError ?? (() => null);
196
+ this.#sleep = options.sleep ?? abortableSleep;
197
+ this.#onFailure = options.onFailure;
198
+ }
199
+
200
+ settle(row: Row): RuntimeSheetRowSettlement {
201
+ const unavailable = this.#unavailableError();
202
+ if (unavailable !== null) return rejectedSettlement(unavailable);
203
+
204
+ const key = normalizedKey(this.#terminalKey(row));
205
+ const bytes = this.#inputBytes(this.#estimateTerminalBytes(row));
206
+ const admitted = deferred();
207
+ const committed = deferred();
208
+ const blocked: BlockedTerminal<Row> = {
209
+ kind: 'terminal',
210
+ key,
211
+ value: row,
212
+ bytes,
213
+ admitted,
214
+ committed,
215
+ };
216
+ if (this.#canAdmit(blocked)) this.#admit(blocked);
217
+ else this.#blocked.push(blocked);
218
+ return {
219
+ admitted: admitted.promise,
220
+ committed: committed.promise,
221
+ };
222
+ }
223
+
224
+ checkpoint(
225
+ updates: readonly Update[] = [],
226
+ ): Promise<RuntimeSheetRowWriterSummary> {
227
+ const unavailable = this.#unavailableError();
228
+ if (unavailable !== null) return Promise.reject(unavailable);
229
+ if (this.#checkpointPromise && updates.length === 0) {
230
+ return this.#checkpointPromise;
231
+ }
232
+
233
+ const admissions = updates.map((update) =>
234
+ this.#enqueueCheckpointUpdate(update),
235
+ );
236
+ const previous = this.#checkpointPromise;
237
+ const operation = (async () => {
238
+ await Promise.all([...(previous ? [previous] : []), ...admissions]);
239
+ await this.#drain();
240
+ return this.#summary();
241
+ })();
242
+ this.#checkpointPromise = operation;
243
+ const clearCheckpoint = () => {
244
+ if (this.#checkpointPromise === operation) {
245
+ this.#checkpointPromise = null;
246
+ }
247
+ };
248
+ void operation.then(clearCheckpoint, clearCheckpoint);
249
+ return operation;
250
+ }
251
+
252
+ finish(): Promise<RuntimeSheetRowWriterSummary> {
253
+ if (this.#finishPromise) return this.#finishPromise;
254
+ if (this.#failure !== null) return Promise.reject(this.#failure);
255
+
256
+ // Close synchronously. No settlement can race in after the terminal
257
+ // barrier has captured the writer's accepted and capacity-blocked work.
258
+ this.#closed = true;
259
+ this.#finishPromise = (async () => {
260
+ if (this.#checkpointPromise) await this.#checkpointPromise;
261
+ await this.#drain();
262
+ return this.#summary();
263
+ })();
264
+ return this.#finishPromise;
265
+ }
266
+
267
+ #enqueueCheckpointUpdate(update: Update): Promise<void> {
268
+ const key = normalizedKey(this.#checkpointKey(update));
269
+ if (this.#terminalAcceptedKeys.has(key)) return Promise.resolve();
270
+ const admitted = deferred();
271
+ const blocked: BlockedCheckpoint<Update> = {
272
+ kind: 'checkpoint',
273
+ key,
274
+ value: update,
275
+ bytes: this.#inputBytes(this.#estimateCheckpointBytes(update)),
276
+ admitted,
277
+ };
278
+ try {
279
+ if (this.#canAdmit(blocked)) this.#admit(blocked);
280
+ else this.#blocked.push(blocked);
281
+ } catch (error) {
282
+ // A merged checkpoint can exceed the item limit even when each patch
283
+ // fits independently. Reject only the incoming checkpoint.
284
+ admitted.reject(error);
285
+ }
286
+ return admitted.promise;
287
+ }
288
+
289
+ #canAdmit(write: BlockedWrite<Row, Update>): boolean {
290
+ const existing = this.#live.get(write.key);
291
+ const replacedBytes =
292
+ existing && (existing.kind === write.kind || write.kind === 'terminal')
293
+ ? existing.bytes
294
+ : 0;
295
+ const admittedBytes =
296
+ write.kind === 'checkpoint' && existing?.kind === 'checkpoint'
297
+ ? this.#inputBytes(
298
+ this.#estimateCheckpointBytes(
299
+ this.#mergeCheckpointUpdates(existing.value, write.value),
300
+ ),
301
+ )
302
+ : write.bytes;
303
+ return (
304
+ this.#bufferedBytes - replacedBytes + admittedBytes <=
305
+ this.#maxBufferedBytes
306
+ );
307
+ }
308
+
309
+ #admit(write: BlockedWrite<Row, Update>): void {
310
+ const existing = this.#live.get(write.key);
311
+ if (write.kind === 'terminal') {
312
+ this.#terminalAcceptedKeys.add(write.key);
313
+ if (existing) {
314
+ this.#liveBytes -= existing.bytes;
315
+ this.#bufferedBytes -= existing.bytes;
316
+ }
317
+ const committed =
318
+ existing?.kind === 'terminal'
319
+ ? [...existing.committed, write.committed]
320
+ : [write.committed];
321
+ const terminal: PendingTerminal<Row> = {
322
+ kind: 'terminal',
323
+ key: write.key,
324
+ value: write.value,
325
+ bytes: write.bytes,
326
+ committed,
327
+ };
328
+ this.#live.set(write.key, terminal);
329
+ } else if (this.#terminalAcceptedKeys.has(write.key)) {
330
+ write.admitted.resolve();
331
+ return;
332
+ } else if (existing?.kind === 'checkpoint') {
333
+ const merged = this.#mergeCheckpointUpdates(existing.value, write.value);
334
+ let mergedBytes: number;
335
+ try {
336
+ mergedBytes = this.#inputBytes(this.#estimateCheckpointBytes(merged));
337
+ } catch (error) {
338
+ write.admitted.reject(error);
339
+ return;
340
+ }
341
+ this.#liveBytes -= existing.bytes;
342
+ this.#bufferedBytes -= existing.bytes;
343
+ const checkpoint: PendingCheckpoint<Update> = {
344
+ kind: 'checkpoint',
345
+ key: write.key,
346
+ value: merged,
347
+ bytes: mergedBytes,
348
+ };
349
+ this.#live.set(write.key, checkpoint);
350
+ write = { ...write, bytes: checkpoint.bytes };
351
+ } else {
352
+ const checkpoint: PendingCheckpoint<Update> = {
353
+ kind: 'checkpoint',
354
+ key: write.key,
355
+ value: write.value,
356
+ bytes: write.bytes,
357
+ };
358
+ this.#live.set(write.key, checkpoint);
359
+ }
360
+ this.#liveBytes += write.bytes;
361
+ this.#bufferedBytes += write.bytes;
362
+ write.admitted.resolve();
363
+ this.#scheduleOrStart();
364
+ this.#notifyStateChanged();
365
+ }
366
+
367
+ #scheduleOrStart(): void {
368
+ if (this.#pump || this.#live.size === 0 || this.#failure !== null) return;
369
+ if (
370
+ this.#live.size >= this.#maxBatchRows ||
371
+ this.#liveBytes >= this.#maxBatchBytes
372
+ ) {
373
+ this.#clearTimer();
374
+ this.#startPump();
375
+ return;
376
+ }
377
+ if (this.#timer) return;
378
+ this.#timer = setTimeout(() => {
379
+ this.#timer = null;
380
+ this.#startPump();
381
+ }, this.#maxFlushMs);
382
+ }
383
+
384
+ #startPump(): void {
385
+ if (this.#pump || this.#live.size === 0 || this.#failure !== null) {
386
+ return;
387
+ }
388
+ this.#pump = this.#runPump().finally(() => {
389
+ this.#pump = null;
390
+ this.#notifyStateChanged();
391
+ if (this.#failure === null && this.#live.size > 0) {
392
+ // Rows accumulated behind an active write are real backlog. Drain the
393
+ // largest available next batch immediately instead of starting a new
394
+ // age window and manufacturing latency.
395
+ this.#startPump();
396
+ }
397
+ });
398
+ }
399
+
400
+ async #runPump(): Promise<void> {
401
+ while (this.#live.size > 0 && this.#failure === null) {
402
+ const batch = this.#takeBatch();
403
+ if (batch.length === 0) return;
404
+ const batchKind = batch[0]!.kind;
405
+ this.#active = batch;
406
+ try {
407
+ const result = await this.#writeIdenticalBatchWithRetry(batch);
408
+ const resultCommitted = result ? result.committed : undefined;
409
+ const resultStaleKeys = result ? result.staleKeys : undefined;
410
+ this.#batches += 1;
411
+ const committed = resultCommitted ?? batch.length;
412
+ if (batchKind === 'terminal') this.#terminalRows += committed;
413
+ else this.#checkpointRows += committed;
414
+ for (const staleKey of resultStaleKeys ?? []) {
415
+ this.#staleKeys.add(staleKey);
416
+ }
417
+ for (const entry of batch) {
418
+ if (entry.kind === 'terminal') {
419
+ entry.committed.forEach((waiter) => waiter.resolve());
420
+ }
421
+ }
422
+ } catch (error) {
423
+ this.#latchFailure(error);
424
+ return;
425
+ } finally {
426
+ const releasedBytes = batch.reduce(
427
+ (total, entry) => total + entry.bytes,
428
+ 0,
429
+ );
430
+ this.#bufferedBytes = Math.max(0, this.#bufferedBytes - releasedBytes);
431
+ this.#active = null;
432
+ this.#admitBlocked();
433
+ this.#notifyStateChanged();
434
+ }
435
+ }
436
+ }
437
+
438
+ #takeBatch(): PendingWrite<Row, Update>[] {
439
+ const first = this.#live.values().next().value as
440
+ | PendingWrite<Row, Update>
441
+ | undefined;
442
+ if (!first) return [];
443
+ const batch: PendingWrite<Row, Update>[] = [];
444
+ const batchKind = first.kind;
445
+ let bytes = 0;
446
+ for (const [key, entry] of this.#live) {
447
+ if (entry.kind !== batchKind) continue;
448
+ if (batch.length >= this.#maxBatchRows) break;
449
+ if (batch.length > 0 && bytes + entry.bytes > this.#maxBatchBytes) break;
450
+ this.#live.delete(key);
451
+ this.#liveBytes -= entry.bytes;
452
+ batch.push(entry);
453
+ bytes += entry.bytes;
454
+ }
455
+ return batch;
456
+ }
457
+
458
+ async #writeIdenticalBatchWithRetry(
459
+ entries: readonly PendingWrite<Row, Update>[],
460
+ ): Promise<RuntimeSheetRowWriterBatchResult | void> {
461
+ const batch: RuntimeSheetRowWriterBatch<Row, Update> =
462
+ entries[0]?.kind === 'checkpoint'
463
+ ? {
464
+ kind: 'checkpoint',
465
+ updates: Object.freeze(
466
+ entries
467
+ .filter(
468
+ (entry): entry is PendingCheckpoint<Update> =>
469
+ entry.kind === 'checkpoint',
470
+ )
471
+ .map((entry) => entry.value),
472
+ ),
473
+ }
474
+ : {
475
+ kind: 'terminal',
476
+ rows: Object.freeze(
477
+ entries
478
+ .filter(
479
+ (entry): entry is PendingTerminal<Row> =>
480
+ entry.kind === 'terminal',
481
+ )
482
+ .map((entry) => entry.value),
483
+ ),
484
+ };
485
+ for (;;) {
486
+ try {
487
+ return await this.#writeBatch(batch, {
488
+ signal: this.#controller.signal,
489
+ });
490
+ } catch (error) {
491
+ const retry = this.#classifyRetryableError(error);
492
+ if (!retry) throw error;
493
+ await this.#sleep(
494
+ Math.max(0, Math.ceil(retry.retryAfterMs)),
495
+ this.#controller.signal,
496
+ );
497
+ }
498
+ }
499
+ }
500
+
501
+ #admitBlocked(): void {
502
+ for (let index = 0; index < this.#blocked.length; ) {
503
+ const write = this.#blocked[index]!;
504
+ let canAdmit = false;
505
+ try {
506
+ canAdmit = this.#canAdmit(write);
507
+ } catch (error) {
508
+ this.#blocked.splice(index, 1);
509
+ write.admitted.reject(error);
510
+ continue;
511
+ }
512
+ if (!canAdmit) {
513
+ index += 1;
514
+ continue;
515
+ }
516
+ this.#blocked.splice(index, 1);
517
+ this.#admit(write);
518
+ }
519
+ }
520
+
521
+ async #drain(): Promise<void> {
522
+ this.#clearTimer();
523
+ for (;;) {
524
+ if (this.#failure !== null) throw this.#failure;
525
+ this.#admitBlocked();
526
+ if (
527
+ this.#active === null &&
528
+ this.#live.size === 0 &&
529
+ this.#blocked.length === 0
530
+ ) {
531
+ return;
532
+ }
533
+ this.#startPump();
534
+ const stateChanged = deferred();
535
+ this.#stateWaiters.push(stateChanged);
536
+ await stateChanged.promise;
537
+ }
538
+ }
539
+
540
+ #latchFailure(error: unknown): void {
541
+ if (this.#failure !== null) return;
542
+ this.#failure = error;
543
+ this.#clearTimer();
544
+ for (const entry of this.#active ?? []) {
545
+ if (entry.kind === 'terminal') {
546
+ entry.committed.forEach((waiter) => waiter.reject(error));
547
+ }
548
+ }
549
+ for (const entry of this.#live.values()) {
550
+ if (entry.kind === 'terminal') {
551
+ entry.committed.forEach((waiter) => waiter.reject(error));
552
+ }
553
+ }
554
+ this.#live.clear();
555
+ this.#liveBytes = 0;
556
+ for (const blocked of this.#blocked.splice(0)) {
557
+ blocked.admitted.reject(error);
558
+ if (blocked.kind === 'terminal') blocked.committed.reject(error);
559
+ }
560
+ this.#onFailure?.(error);
561
+ this.#notifyStateChanged(error);
562
+ }
563
+
564
+ #unavailableError(): unknown | null {
565
+ if (this.#failure !== null) return this.#failure;
566
+ return this.#closed ? new RuntimeSheetRowWriterClosedError() : null;
567
+ }
568
+
569
+ #inputBytes(estimated: number): number {
570
+ const bytes = Math.max(0, Math.ceil(estimated));
571
+ if (bytes > this.#maxBatchBytes || bytes > this.#maxBufferedBytes) {
572
+ throw new RuntimeSheetRowWriterBufferLimitError(
573
+ bytes,
574
+ Math.min(this.#maxBatchBytes, this.#maxBufferedBytes),
575
+ );
576
+ }
577
+ return bytes;
578
+ }
579
+
580
+ #summary(): RuntimeSheetRowWriterSummary {
581
+ return {
582
+ batches: this.#batches,
583
+ checkpointRows: this.#checkpointRows,
584
+ staleKeys: [...this.#staleKeys],
585
+ terminalRows: this.#terminalRows,
586
+ };
587
+ }
588
+
589
+ #clearTimer(): void {
590
+ if (!this.#timer) return;
591
+ clearTimeout(this.#timer);
592
+ this.#timer = null;
593
+ }
594
+
595
+ #notifyStateChanged(error?: unknown): void {
596
+ const waiters = this.#stateWaiters.splice(0);
597
+ if (error === undefined) {
598
+ for (const waiter of waiters) {
599
+ waiter.resolve();
600
+ }
601
+ return;
602
+ }
603
+ for (const waiter of waiters) {
604
+ waiter.reject(error);
605
+ }
606
+ }
607
+ }
608
+
609
+ function deferred(): Deferred {
610
+ let resolve!: () => void;
611
+ let reject!: (error: unknown) => void;
612
+ const promise = new Promise<void>((resolvePromise, rejectPromise) => {
613
+ resolve = resolvePromise;
614
+ reject = rejectPromise;
615
+ });
616
+ return { promise, reject, resolve };
617
+ }
618
+
619
+ function rejectedSettlement(error: unknown): RuntimeSheetRowSettlement {
620
+ const rejected = Promise.reject<void>(error);
621
+ return { admitted: rejected, committed: rejected };
622
+ }
623
+
624
+ function normalizedKey(value: string): string {
625
+ const key = value.trim();
626
+ if (!key) throw new Error('Runtime Sheet row writer requires a row key.');
627
+ return key;
628
+ }
629
+
630
+ function positiveInteger(value: number | undefined, fallback: number): number {
631
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
632
+ return fallback;
633
+ }
634
+ return Math.max(1, Math.floor(value));
635
+ }
636
+
637
+ function estimateJsonBytes(value: unknown): number {
638
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
639
+ }
640
+
641
+ async function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
642
+ if (ms <= 0) {
643
+ await Promise.resolve();
644
+ return;
645
+ }
646
+ await new Promise<void>((resolve, reject) => {
647
+ let settled = false;
648
+ const finish = (error?: unknown) => {
649
+ if (settled) return;
650
+ settled = true;
651
+ clearTimeout(timer);
652
+ signal.removeEventListener('abort', onAbort);
653
+ if (error === undefined) resolve();
654
+ else reject(error);
655
+ };
656
+ const onAbort = () =>
657
+ finish(signal.reason ?? new Error('Runtime Sheet row writer aborted.'));
658
+ const timer = setTimeout(() => finish(), ms);
659
+ signal.addEventListener('abort', onAbort, { once: true });
660
+ if (signal.aborted) onAbort();
661
+ });
662
+ }
package/dist/cli/index.js CHANGED
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
718
718
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
719
719
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
720
720
  // Operators use the checkout-local deepline-admin binary instead.
721
- version: "0.1.294",
721
+ version: "0.1.296",
722
722
  contracts: {
723
723
  api: {
724
724
  name: "sdk-http-api",
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
703
703
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
704
704
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
705
705
  // Operators use the checkout-local deepline-admin binary instead.
706
- version: "0.1.294",
706
+ version: "0.1.296",
707
707
  contracts: {
708
708
  api: {
709
709
  name: "sdk-http-api",
package/dist/index.js CHANGED
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
438
438
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
439
439
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
440
440
  // Operators use the checkout-local deepline-admin binary instead.
441
- version: "0.1.294",
441
+ version: "0.1.296",
442
442
  contracts: {
443
443
  api: {
444
444
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
367
367
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
368
368
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
369
369
  // Operators use the checkout-local deepline-admin binary instead.
370
- version: "0.1.294",
370
+ version: "0.1.296",
371
371
  contracts: {
372
372
  api: {
373
373
  name: "sdk-http-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.294",
3
+ "version": "0.1.296",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {