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.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +237 -167
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +15 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +83 -54
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/pacing.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +3 -94
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +191 -22
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +662 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
721
|
+
version: "0.1.296",
|
|
722
722
|
contracts: {
|
|
723
723
|
api: {
|
|
724
724
|
name: "sdk-http-api",
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
370
|
+
version: "0.1.296",
|
|
371
371
|
contracts: {
|
|
372
372
|
api: {
|
|
373
373
|
name: "sdk-http-api",
|