deepline 0.1.24 → 0.1.26
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/cli/index.js +344 -179
- package/dist/cli/index.mjs +296 -130
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +75 -62
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +6 -1
- package/dist/repo/apps/play-runner-workers/src/entry.ts +1102 -711
- package/dist/repo/apps/play-runner-workers/src/runtime/dataset-handles.ts +418 -0
- package/dist/repo/sdk/src/client.ts +5 -1
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +1 -1
- package/dist/repo/sdk/src/plays/harness-stub.ts +23 -35
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/play-runtime/execution-plan.ts +18 -8
- package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +5 -4
- package/dist/repo/shared_libs/play-runtime/step-lifecycle-tracker.ts +228 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +90 -51
- package/package.json +1 -1
- package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +0 -208
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCsvRenameProjection,
|
|
3
|
+
type CsvRenameOptions,
|
|
4
|
+
} from '../../../../shared_libs/play-runtime/csv-rename';
|
|
5
|
+
import {
|
|
6
|
+
createDeferredPlayDataset,
|
|
7
|
+
isPlayDataset,
|
|
8
|
+
type PlayDataset,
|
|
9
|
+
type PlayDatasetInput,
|
|
10
|
+
type PlayDatasetKind,
|
|
11
|
+
type PlayDatasetWorkProgressSummary,
|
|
12
|
+
} from '../../../../shared_libs/plays/dataset';
|
|
13
|
+
|
|
14
|
+
export const WORKER_DATASET_PREVIEW_ROWS = 5;
|
|
15
|
+
export const WORKER_DATASET_IN_MEMORY_ROWS = 100;
|
|
16
|
+
const STREAM_MATERIALIZE_CHUNK_ROWS = 5_000;
|
|
17
|
+
const STREAM_ITERATE_CHUNK_ROWS = 1_000;
|
|
18
|
+
|
|
19
|
+
export type DatasetRow = Record<string, unknown>;
|
|
20
|
+
export type WorkerDatasetHandle<T extends DatasetRow> = PlayDataset<T>;
|
|
21
|
+
export type WorkerDatasetInput<T extends DatasetRow> = PlayDatasetInput<T>;
|
|
22
|
+
|
|
23
|
+
type DatasetChunkReader<T extends DatasetRow> = (
|
|
24
|
+
chunkSize: number,
|
|
25
|
+
) => AsyncIterable<T[]>;
|
|
26
|
+
|
|
27
|
+
const datasetChunkReaders = new WeakMap<object, DatasetChunkReader<DatasetRow>>();
|
|
28
|
+
const datasetCountHints = new WeakMap<object, number | null>();
|
|
29
|
+
|
|
30
|
+
function cloneRow<T extends DatasetRow>(row: T): T {
|
|
31
|
+
return { ...row };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerChunkReader<T extends DatasetRow>(
|
|
35
|
+
dataset: PlayDataset<T>,
|
|
36
|
+
reader: DatasetChunkReader<T>,
|
|
37
|
+
countHint: number | null,
|
|
38
|
+
): PlayDataset<T> {
|
|
39
|
+
datasetChunkReaders.set(
|
|
40
|
+
dataset as object,
|
|
41
|
+
reader as DatasetChunkReader<DatasetRow>,
|
|
42
|
+
);
|
|
43
|
+
datasetCountHints.set(dataset as object, countHint);
|
|
44
|
+
return dataset;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizedChunkSize(chunkSize: number): number {
|
|
48
|
+
return Math.max(1, Math.floor(chunkSize));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createMaterializedDatasetHandle<T extends DatasetRow>(input: {
|
|
52
|
+
name: string;
|
|
53
|
+
rows: readonly T[];
|
|
54
|
+
datasetKind?: PlayDatasetKind;
|
|
55
|
+
count?: number;
|
|
56
|
+
sourceLabel?: string | null;
|
|
57
|
+
workProgress?: PlayDatasetWorkProgressSummary;
|
|
58
|
+
}): WorkerDatasetHandle<T> {
|
|
59
|
+
const datasetKind = input.datasetKind ?? 'map';
|
|
60
|
+
const materializedRows = input.rows.map(cloneRow);
|
|
61
|
+
const dataset = createDeferredPlayDataset({
|
|
62
|
+
datasetKind,
|
|
63
|
+
datasetId: `${datasetKind}:${input.name}`,
|
|
64
|
+
count: Math.max(0, Math.floor(input.count ?? materializedRows.length)),
|
|
65
|
+
previewRows: materializedRows
|
|
66
|
+
.slice(0, WORKER_DATASET_PREVIEW_ROWS)
|
|
67
|
+
.map(cloneRow),
|
|
68
|
+
sourceLabel: input.sourceLabel ?? null,
|
|
69
|
+
tableNamespace: input.name,
|
|
70
|
+
workProgress: input.workProgress,
|
|
71
|
+
resolvers: {
|
|
72
|
+
count: async () =>
|
|
73
|
+
Math.max(0, Math.floor(input.count ?? materializedRows.length)),
|
|
74
|
+
peek: async (limit) => materializedRows.slice(0, Math.max(0, limit)),
|
|
75
|
+
materialize: async (limit) =>
|
|
76
|
+
limit === undefined
|
|
77
|
+
? materializedRows.map(cloneRow)
|
|
78
|
+
: materializedRows.slice(0, Math.max(0, limit)).map(cloneRow),
|
|
79
|
+
iterate: () =>
|
|
80
|
+
({
|
|
81
|
+
async *[Symbol.asyncIterator]() {
|
|
82
|
+
for (const row of materializedRows) {
|
|
83
|
+
yield cloneRow(row);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}) as AsyncIterable<T>,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return registerChunkReader(
|
|
90
|
+
dataset,
|
|
91
|
+
async function* (chunkSize: number) {
|
|
92
|
+
const size = normalizedChunkSize(chunkSize);
|
|
93
|
+
for (let offset = 0; offset < materializedRows.length; offset += size) {
|
|
94
|
+
yield materializedRows.slice(offset, offset + size).map(cloneRow);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
Math.max(0, Math.floor(input.count ?? materializedRows.length)),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
|
|
102
|
+
playName: string;
|
|
103
|
+
name: string;
|
|
104
|
+
count: number;
|
|
105
|
+
datasetKind?: PlayDatasetKind;
|
|
106
|
+
previewRows?: readonly T[];
|
|
107
|
+
cachedRows?: readonly T[] | null;
|
|
108
|
+
sourceLabel?: string | null;
|
|
109
|
+
workProgress?: PlayDatasetWorkProgressSummary;
|
|
110
|
+
readRows: (input: { limit: number; offset: number }) => Promise<readonly T[]>;
|
|
111
|
+
trace?: (
|
|
112
|
+
phase: string,
|
|
113
|
+
ms: number,
|
|
114
|
+
extra?: Record<string, unknown>,
|
|
115
|
+
) => void;
|
|
116
|
+
nowMs: () => number;
|
|
117
|
+
}): WorkerDatasetHandle<T> {
|
|
118
|
+
const datasetKind = input.datasetKind ?? 'map';
|
|
119
|
+
const count = Math.max(0, Math.floor(input.count));
|
|
120
|
+
const previewRows = (input.previewRows ?? [])
|
|
121
|
+
.slice(0, WORKER_DATASET_PREVIEW_ROWS)
|
|
122
|
+
.map(cloneRow);
|
|
123
|
+
const cachedRows =
|
|
124
|
+
input.cachedRows && input.cachedRows.length <= WORKER_DATASET_IN_MEMORY_ROWS
|
|
125
|
+
? input.cachedRows.map(cloneRow)
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
async function loadRows(limit: number, offset: number): Promise<T[]> {
|
|
129
|
+
if (limit <= 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const normalizedLimit = normalizedChunkSize(limit);
|
|
133
|
+
const normalizedOffset = Math.max(0, Math.floor(offset));
|
|
134
|
+
if (
|
|
135
|
+
cachedRows &&
|
|
136
|
+
normalizedOffset < cachedRows.length &&
|
|
137
|
+
normalizedOffset + normalizedLimit <= cachedRows.length
|
|
138
|
+
) {
|
|
139
|
+
return cachedRows
|
|
140
|
+
.slice(normalizedOffset, normalizedOffset + normalizedLimit)
|
|
141
|
+
.map(cloneRow);
|
|
142
|
+
}
|
|
143
|
+
const startedAt = input.nowMs();
|
|
144
|
+
const rows = await input.readRows({
|
|
145
|
+
limit: normalizedLimit,
|
|
146
|
+
offset: normalizedOffset,
|
|
147
|
+
});
|
|
148
|
+
input.trace?.('dataset.read', input.nowMs() - startedAt, {
|
|
149
|
+
datasetKind,
|
|
150
|
+
playName: input.playName,
|
|
151
|
+
tableNamespace: input.name,
|
|
152
|
+
limit: normalizedLimit,
|
|
153
|
+
offset: normalizedOffset,
|
|
154
|
+
rows: rows.length,
|
|
155
|
+
});
|
|
156
|
+
return rows.map(cloneRow);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function* readChunks(chunkSize: number): AsyncGenerator<T[], void, void> {
|
|
160
|
+
const size = normalizedChunkSize(chunkSize);
|
|
161
|
+
let offset = 0;
|
|
162
|
+
while (offset < count) {
|
|
163
|
+
const rows = await loadRows(Math.min(size, count - offset), offset);
|
|
164
|
+
if (rows.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
yield rows;
|
|
168
|
+
offset += rows.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const dataset = createDeferredPlayDataset({
|
|
173
|
+
datasetKind,
|
|
174
|
+
datasetId: `${datasetKind}:${input.name}`,
|
|
175
|
+
count,
|
|
176
|
+
backing: {
|
|
177
|
+
storage: 'neon_sheet',
|
|
178
|
+
sheet: {
|
|
179
|
+
playName: input.playName,
|
|
180
|
+
tableNamespace: input.name,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
previewRows,
|
|
184
|
+
sourceLabel: input.sourceLabel ?? null,
|
|
185
|
+
tableNamespace: input.name,
|
|
186
|
+
workProgress: input.workProgress,
|
|
187
|
+
resolvers: {
|
|
188
|
+
count: async () => count,
|
|
189
|
+
peek: async (limit) => await loadRows(Math.max(0, limit), 0),
|
|
190
|
+
materialize: async (limit) => {
|
|
191
|
+
const rows: T[] = [];
|
|
192
|
+
const maxRows = limit ?? count;
|
|
193
|
+
for await (const chunk of readChunks(STREAM_MATERIALIZE_CHUNK_ROWS)) {
|
|
194
|
+
for (const row of chunk) {
|
|
195
|
+
if (rows.length >= maxRows) return rows;
|
|
196
|
+
rows.push(row);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return rows;
|
|
200
|
+
},
|
|
201
|
+
iterate: () =>
|
|
202
|
+
({
|
|
203
|
+
async *[Symbol.asyncIterator]() {
|
|
204
|
+
for await (const chunk of readChunks(STREAM_ITERATE_CHUNK_ROWS)) {
|
|
205
|
+
for (const row of chunk) yield row;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
}) as AsyncIterable<T>,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
return registerChunkReader(dataset, (chunkSize) => readChunks(chunkSize), count);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createCsvDatasetHandle<T extends DatasetRow>(input: {
|
|
215
|
+
name: string;
|
|
216
|
+
logicalPath: string;
|
|
217
|
+
expectedBytes?: number | null;
|
|
218
|
+
renameOptions?: CsvRenameOptions;
|
|
219
|
+
open: () => Promise<AsyncIterable<Uint8Array> | null>;
|
|
220
|
+
streamRows: (
|
|
221
|
+
byteChunks: AsyncIterable<Uint8Array>,
|
|
222
|
+
chunkSize: number,
|
|
223
|
+
) => AsyncIterable<T[]>;
|
|
224
|
+
trace?: (
|
|
225
|
+
phase: string,
|
|
226
|
+
ms: number,
|
|
227
|
+
extra?: Record<string, unknown>,
|
|
228
|
+
) => void;
|
|
229
|
+
nowMs: () => number;
|
|
230
|
+
}): WorkerDatasetHandle<T> {
|
|
231
|
+
const datasetId = `csv:${input.name}`;
|
|
232
|
+
let cachedCount: number | null = null;
|
|
233
|
+
|
|
234
|
+
async function* readChunks(chunkSize: number): AsyncGenerator<T[], void, void> {
|
|
235
|
+
const startedAt = input.nowMs();
|
|
236
|
+
let yieldedRows = 0;
|
|
237
|
+
let yieldedChunks = 0;
|
|
238
|
+
let observedBytes = 0;
|
|
239
|
+
let observedByteChunks = 0;
|
|
240
|
+
const body = await input.open();
|
|
241
|
+
if (!body) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`ctx.csv("${input.logicalPath}"): dataset source is not reachable from the play runner.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const size = normalizedChunkSize(chunkSize);
|
|
247
|
+
const countedBody = {
|
|
248
|
+
async *[Symbol.asyncIterator]() {
|
|
249
|
+
for await (const chunk of body) {
|
|
250
|
+
observedBytes += chunk.byteLength;
|
|
251
|
+
observedByteChunks += 1;
|
|
252
|
+
yield chunk;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
} satisfies AsyncIterable<Uint8Array>;
|
|
256
|
+
try {
|
|
257
|
+
for await (const chunk of input.streamRows(countedBody, size)) {
|
|
258
|
+
yieldedRows += chunk.length;
|
|
259
|
+
yieldedChunks += 1;
|
|
260
|
+
yield applyCsvRenameProjection(chunk, input.renameOptions) as T[];
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
input.trace?.('csv.stream', input.nowMs() - startedAt, {
|
|
264
|
+
name: input.name,
|
|
265
|
+
logicalPath: input.logicalPath,
|
|
266
|
+
expectedBytes: input.expectedBytes ?? null,
|
|
267
|
+
observedBytes,
|
|
268
|
+
observedByteChunks,
|
|
269
|
+
yieldedRows,
|
|
270
|
+
yieldedChunks,
|
|
271
|
+
chunkSize: size,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
typeof input.expectedBytes === 'number' &&
|
|
276
|
+
input.expectedBytes > 0 &&
|
|
277
|
+
yieldedRows === 0
|
|
278
|
+
) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
observedBytes === 0
|
|
281
|
+
? `ctx.csv("${input.logicalPath}"): dataset source yielded 0 bytes for ` +
|
|
282
|
+
`a non-empty ${input.expectedBytes} byte staged file.`
|
|
283
|
+
: `ctx.csv("${input.logicalPath}"): parsed 0 rows after reading ` +
|
|
284
|
+
`${observedBytes} bytes from a non-empty ${input.expectedBytes} byte dataset source.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const dataset = createDeferredPlayDataset({
|
|
290
|
+
datasetKind: 'csv',
|
|
291
|
+
datasetId,
|
|
292
|
+
count: 0,
|
|
293
|
+
previewRows: [],
|
|
294
|
+
tableNamespace: input.name,
|
|
295
|
+
resolvers: {
|
|
296
|
+
count: async () => {
|
|
297
|
+
if (cachedCount !== null) return cachedCount;
|
|
298
|
+
let total = 0;
|
|
299
|
+
for await (const chunk of readChunks(STREAM_MATERIALIZE_CHUNK_ROWS)) {
|
|
300
|
+
total += chunk.length;
|
|
301
|
+
}
|
|
302
|
+
cachedCount = total;
|
|
303
|
+
return total;
|
|
304
|
+
},
|
|
305
|
+
peek: async (limit) => {
|
|
306
|
+
const rows: T[] = [];
|
|
307
|
+
for await (const chunk of readChunks(Math.max(1, limit))) {
|
|
308
|
+
for (const row of chunk) {
|
|
309
|
+
rows.push(row);
|
|
310
|
+
if (rows.length >= limit) return rows;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return rows;
|
|
314
|
+
},
|
|
315
|
+
materialize: async (limit) => {
|
|
316
|
+
const rows: T[] = [];
|
|
317
|
+
const maxRows = limit ?? Number.POSITIVE_INFINITY;
|
|
318
|
+
for await (const chunk of readChunks(STREAM_MATERIALIZE_CHUNK_ROWS)) {
|
|
319
|
+
for (const row of chunk) {
|
|
320
|
+
if (rows.length >= maxRows) return rows;
|
|
321
|
+
rows.push(row);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return rows;
|
|
325
|
+
},
|
|
326
|
+
iterate: () =>
|
|
327
|
+
({
|
|
328
|
+
async *[Symbol.asyncIterator]() {
|
|
329
|
+
for await (const chunk of readChunks(STREAM_ITERATE_CHUNK_ROWS)) {
|
|
330
|
+
for (const row of chunk) yield row;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
}) as AsyncIterable<T>,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
return registerChunkReader(dataset, (chunkSize) => readChunks(chunkSize), null);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function createInlineDatasetHandle<T extends DatasetRow>(
|
|
340
|
+
rows: readonly T[],
|
|
341
|
+
metadata?: {
|
|
342
|
+
name?: string;
|
|
343
|
+
kind?: PlayDatasetKind;
|
|
344
|
+
sourceLabel?: string | null;
|
|
345
|
+
},
|
|
346
|
+
): WorkerDatasetHandle<T> {
|
|
347
|
+
return createMaterializedDatasetHandle({
|
|
348
|
+
name: metadata?.name ?? 'csv',
|
|
349
|
+
rows,
|
|
350
|
+
datasetKind: metadata?.kind ?? 'csv',
|
|
351
|
+
sourceLabel: metadata?.sourceLabel ?? null,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function datasetRowCountHint<T extends DatasetRow>(
|
|
356
|
+
input: WorkerDatasetInput<T>,
|
|
357
|
+
): number | null {
|
|
358
|
+
if (Array.isArray(input)) return input.length;
|
|
359
|
+
if (isPlayDataset<T>(input)) {
|
|
360
|
+
const hinted = datasetCountHints.get(input as object);
|
|
361
|
+
if (hinted !== undefined) return hinted;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function isDatasetHandle<T extends DatasetRow>(
|
|
367
|
+
value: unknown,
|
|
368
|
+
): value is WorkerDatasetHandle<T> {
|
|
369
|
+
return isPlayDataset<T>(value);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function iterDatasetChunks<T extends DatasetRow>(
|
|
373
|
+
input: WorkerDatasetInput<T>,
|
|
374
|
+
chunkSize: number,
|
|
375
|
+
): AsyncIterable<T[]> {
|
|
376
|
+
const size = normalizedChunkSize(chunkSize);
|
|
377
|
+
if (isPlayDataset<T>(input)) {
|
|
378
|
+
const chunkReader = datasetChunkReaders.get(input as object);
|
|
379
|
+
if (chunkReader) {
|
|
380
|
+
return chunkReader(size) as AsyncIterable<T[]>;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
async *[Symbol.asyncIterator]() {
|
|
385
|
+
let chunk: T[] = [];
|
|
386
|
+
const flush = function* () {
|
|
387
|
+
if (chunk.length > 0) {
|
|
388
|
+
yield chunk;
|
|
389
|
+
chunk = [];
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const push = function* (row: T) {
|
|
393
|
+
chunk.push(row);
|
|
394
|
+
if (chunk.length >= size) {
|
|
395
|
+
yield chunk;
|
|
396
|
+
chunk = [];
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (isPlayDataset<T>(input)) {
|
|
401
|
+
for await (const row of input) yield* push(row);
|
|
402
|
+
yield* flush();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (
|
|
406
|
+
input != null &&
|
|
407
|
+
typeof input === 'object' &&
|
|
408
|
+
Symbol.asyncIterator in input
|
|
409
|
+
) {
|
|
410
|
+
for await (const row of input as AsyncIterable<T>) yield* push(row);
|
|
411
|
+
yield* flush();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
for (const row of input as Iterable<T>) yield* push(row);
|
|
415
|
+
yield* flush();
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
@@ -225,7 +225,11 @@ function updatePlayLiveStatusState(
|
|
|
225
225
|
: state.runId;
|
|
226
226
|
const status = normalizeLiveStatus(payload.status) ?? state.status;
|
|
227
227
|
const logs = readStringArray(payload.logs);
|
|
228
|
-
if (
|
|
228
|
+
if (
|
|
229
|
+
logs.length > 0 ||
|
|
230
|
+
event.type === 'play.run.snapshot' ||
|
|
231
|
+
event.type === 'play.run.final_status'
|
|
232
|
+
) {
|
|
229
233
|
state.logs = logs;
|
|
230
234
|
}
|
|
231
235
|
if ('result' in payload) {
|
|
@@ -43,7 +43,7 @@ export {
|
|
|
43
43
|
extractDefinedPlayName,
|
|
44
44
|
} from '../../../shared_libs/plays/bundling/index.js';
|
|
45
45
|
|
|
46
|
-
const PLAY_BUNDLE_CACHE_VERSION =
|
|
46
|
+
const PLAY_BUNDLE_CACHE_VERSION = 30;
|
|
47
47
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
48
48
|
const SDK_PACKAGE_ROOT = resolve(MODULE_DIR, '..', '..');
|
|
49
49
|
const SOURCE_REPO_ROOT = resolve(SDK_PACKAGE_ROOT, '..');
|
|
@@ -37,6 +37,10 @@ import type {
|
|
|
37
37
|
RuntimeApiCallInput,
|
|
38
38
|
RuntimeApiCallResult,
|
|
39
39
|
RuntimePayloadSchemaId,
|
|
40
|
+
SheetDatasetRowsInput,
|
|
41
|
+
SheetDatasetRowsResult,
|
|
42
|
+
StagedFileChunkInput,
|
|
43
|
+
StagedFileChunkResult,
|
|
40
44
|
ValidatePayloadResult,
|
|
41
45
|
} from '../../../apps/play-harness-worker/src/rpc-types';
|
|
42
46
|
|
|
@@ -46,9 +50,6 @@ import type {
|
|
|
46
50
|
* promise-returning RPC stubs; the type matches `PlayHarnessRpc`.
|
|
47
51
|
*/
|
|
48
52
|
export type HarnessBinding = PlayHarnessRpc;
|
|
49
|
-
type HarnessFetchBinding = HarnessBinding & {
|
|
50
|
-
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
|
51
|
-
};
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Module-level holder for the binding. Set by the per-play harness on
|
|
@@ -122,6 +123,24 @@ export async function harnessRuntimeApiCall(
|
|
|
122
123
|
return requireBinding().runtimeApiCall(input);
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Read a bounded staged-file byte range through typed harness RPC. This is the
|
|
128
|
+
* only staged-file data path for per-play Workers; there is intentionally no
|
|
129
|
+
* HTTP fetch fallback because stale parallel paths caused empty CSV streams in
|
|
130
|
+
* preview.
|
|
131
|
+
*/
|
|
132
|
+
export async function harnessReadStagedFileChunk(
|
|
133
|
+
input: StagedFileChunkInput,
|
|
134
|
+
): Promise<StagedFileChunkResult> {
|
|
135
|
+
return requireBinding().readStagedFileChunk(input);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function harnessReadSheetDatasetRows(
|
|
139
|
+
input: SheetDatasetRowsInput,
|
|
140
|
+
): Promise<SheetDatasetRowsResult> {
|
|
141
|
+
return requireBinding().readSheetDatasetRows(input);
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
/**
|
|
126
145
|
* Warm Postgres sessions in the long-lived harness Worker. This preserves the
|
|
127
146
|
* map fast path without bundling the Neon client into every dynamic play.
|
|
@@ -145,6 +164,7 @@ export async function harnessStartSheetDataset(input: {
|
|
|
145
164
|
sheetContract: unknown;
|
|
146
165
|
rows: Array<Record<string, unknown>>;
|
|
147
166
|
runId: string;
|
|
167
|
+
inputOffset?: number;
|
|
148
168
|
userEmail?: string | null;
|
|
149
169
|
preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
|
|
150
170
|
}): Promise<{
|
|
@@ -176,38 +196,6 @@ export async function harnessPersistCompletedSheetRows(input: {
|
|
|
176
196
|
return requireBinding().persistCompletedMapRows(input);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
/**
|
|
180
|
-
* Stream a staged R2 object through the harness Worker. Unlike the legacy
|
|
181
|
-
* signed-URL helper this never touches the Vercel runtime route, and it keeps
|
|
182
|
-
* large CSV bodies as streams instead of serializing them through RPC.
|
|
183
|
-
*/
|
|
184
|
-
export async function harnessFetchStagedFile(input: {
|
|
185
|
-
executorToken: string;
|
|
186
|
-
storageKey: string;
|
|
187
|
-
method?: 'GET' | 'HEAD';
|
|
188
|
-
range?: { offset: number; length: number };
|
|
189
|
-
}): Promise<Response> {
|
|
190
|
-
const binding = requireBinding() as HarnessFetchBinding;
|
|
191
|
-
if (typeof binding.fetch !== 'function') {
|
|
192
|
-
throw new Error(
|
|
193
|
-
'[harness-stub] env.HARNESS does not expose fetch(); cannot stream staged R2 files through the harness.',
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const url = new URL('https://play-harness.internal/r2/staged-file');
|
|
197
|
-
url.searchParams.set('storageKey', input.storageKey);
|
|
198
|
-
const headers: Record<string, string> = {
|
|
199
|
-
authorization: `Bearer ${input.executorToken}`,
|
|
200
|
-
};
|
|
201
|
-
if (input.range) {
|
|
202
|
-
const end = input.range.offset + input.range.length - 1;
|
|
203
|
-
headers.range = `bytes=${input.range.offset}-${end}`;
|
|
204
|
-
}
|
|
205
|
-
return binding.fetch(url.toString(), {
|
|
206
|
-
method: input.method ?? 'GET',
|
|
207
|
-
headers,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
199
|
/**
|
|
212
200
|
* Validate a payload against a named schema. The schema definitions
|
|
213
201
|
* (and zod itself) live in the harness Worker — see
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.1.
|
|
1
|
+
export const SDK_VERSION = "0.1.26";
|
|
2
2
|
export const SDK_API_CONTRACT = "2026-05-runs-v2";
|
|
@@ -126,13 +126,29 @@ export function buildExecutionPlan(
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export function chooseMapChunkSize(input: {
|
|
129
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Known input row count. Pass null for streaming datasets where counting
|
|
131
|
+
* would require an extra full scan; unknown counts use the preferred chunk.
|
|
132
|
+
*/
|
|
133
|
+
totalRows: number | null;
|
|
130
134
|
mapCount: number;
|
|
131
135
|
stepsPerChunk: number;
|
|
132
136
|
preferredChunkSize?: number | null;
|
|
133
137
|
softWorkflowStepBudget?: number | null;
|
|
134
138
|
}): number {
|
|
135
|
-
const totalRows =
|
|
139
|
+
const totalRows =
|
|
140
|
+
typeof input.totalRows === 'number' && Number.isFinite(input.totalRows)
|
|
141
|
+
? Math.max(0, Math.floor(input.totalRows))
|
|
142
|
+
: null;
|
|
143
|
+
const preferred = Math.max(
|
|
144
|
+
1,
|
|
145
|
+
Math.floor(
|
|
146
|
+
input.preferredChunkSize ?? EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
if (totalRows === null) {
|
|
150
|
+
return preferred;
|
|
151
|
+
}
|
|
136
152
|
if (totalRows <= EXECUTION_PLAN_DEFAULTS.inlineRowsLimit) {
|
|
137
153
|
return Math.max(1, totalRows || 1);
|
|
138
154
|
}
|
|
@@ -157,12 +173,6 @@ export function chooseMapChunkSize(input: {
|
|
|
157
173
|
1,
|
|
158
174
|
Math.ceil(totalRows / maxChunksPerMap),
|
|
159
175
|
);
|
|
160
|
-
const preferred = Math.max(
|
|
161
|
-
1,
|
|
162
|
-
Math.floor(
|
|
163
|
-
input.preferredChunkSize ?? EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
176
|
return Math.max(preferred, minimumSurvivalChunkSize);
|
|
167
177
|
}
|
|
168
178
|
|
|
@@ -52,13 +52,14 @@ export type PlaySchedulerSubmitInput = {
|
|
|
52
52
|
storageKey?: string;
|
|
53
53
|
fileName?: string;
|
|
54
54
|
logicalPath?: string;
|
|
55
|
+
contentType?: string;
|
|
56
|
+
bytes?: number;
|
|
55
57
|
} | null;
|
|
56
58
|
inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* Temporal-side `inlineInputFile` fast path.
|
|
60
|
+
* Temporal-only escape hatch for small legacy inputs. Workers-edge CSV
|
|
61
|
+
* inputs must be staged and streamed by handle instead of carried as raw
|
|
62
|
+
* scheduler payload bytes.
|
|
62
63
|
*/
|
|
63
64
|
inlineInputFile?: {
|
|
64
65
|
logicalPath: string;
|