deepline 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +324 -0
  2. package/dist/cli/index.js +6750 -503
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/index.mjs +6735 -512
  5. package/dist/cli/index.mjs.map +1 -1
  6. package/dist/index.d.mts +2349 -32
  7. package/dist/index.d.ts +2349 -32
  8. package/dist/index.js +1631 -82
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +1617 -83
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +3256 -0
  13. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +710 -0
  14. package/dist/repo/apps/play-runner-workers/src/entry.ts +5070 -0
  15. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +21 -0
  16. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +177 -0
  17. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +52 -0
  18. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +100 -0
  19. package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +184 -0
  20. package/dist/repo/sdk/src/cli/commands/auth.ts +482 -0
  21. package/dist/repo/sdk/src/cli/commands/billing.ts +188 -0
  22. package/dist/repo/sdk/src/cli/commands/csv.ts +123 -0
  23. package/dist/repo/sdk/src/cli/commands/db.ts +119 -0
  24. package/dist/repo/sdk/src/cli/commands/feedback.ts +40 -0
  25. package/dist/repo/sdk/src/cli/commands/org.ts +117 -0
  26. package/dist/repo/sdk/src/cli/commands/play.ts +3200 -0
  27. package/dist/repo/sdk/src/cli/commands/tools.ts +687 -0
  28. package/dist/repo/sdk/src/cli/dataset-stats.ts +341 -0
  29. package/dist/repo/sdk/src/cli/index.ts +138 -0
  30. package/dist/repo/sdk/src/cli/progress.ts +135 -0
  31. package/dist/repo/sdk/src/cli/trace.ts +61 -0
  32. package/dist/repo/sdk/src/cli/utils.ts +145 -0
  33. package/dist/repo/sdk/src/client.ts +1188 -0
  34. package/dist/repo/sdk/src/compat.ts +77 -0
  35. package/dist/repo/sdk/src/config.ts +285 -0
  36. package/dist/repo/sdk/src/errors.ts +125 -0
  37. package/dist/repo/sdk/src/http.ts +391 -0
  38. package/dist/repo/sdk/src/index.ts +139 -0
  39. package/dist/repo/sdk/src/play.ts +1330 -0
  40. package/dist/repo/sdk/src/plays/bundle-play-file.ts +133 -0
  41. package/dist/repo/sdk/src/plays/harness-stub.ts +210 -0
  42. package/dist/repo/sdk/src/plays/local-file-discovery.ts +326 -0
  43. package/dist/repo/sdk/src/tool-output.ts +489 -0
  44. package/dist/repo/sdk/src/types.ts +669 -0
  45. package/dist/repo/sdk/src/version.ts +2 -0
  46. package/dist/repo/sdk/src/worker-play-entry.ts +286 -0
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +129 -0
  48. package/dist/repo/shared_libs/observability/tracing.ts +98 -0
  49. package/dist/repo/shared_libs/play-runtime/backend.ts +139 -0
  50. package/dist/repo/shared_libs/play-runtime/batch-runtime.ts +182 -0
  51. package/dist/repo/shared_libs/play-runtime/batching-types.ts +91 -0
  52. package/dist/repo/shared_libs/play-runtime/context.ts +3999 -0
  53. package/dist/repo/shared_libs/play-runtime/coordinator-headers.ts +78 -0
  54. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +250 -0
  55. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +713 -0
  56. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +10 -0
  57. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +304 -0
  58. package/dist/repo/shared_libs/play-runtime/db-session.ts +462 -0
  59. package/dist/repo/shared_libs/play-runtime/dedup-backend.ts +0 -0
  60. package/dist/repo/shared_libs/play-runtime/default-batch-strategies.ts +124 -0
  61. package/dist/repo/shared_libs/play-runtime/execution-plan.ts +262 -0
  62. package/dist/repo/shared_libs/play-runtime/live-events.ts +214 -0
  63. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +50 -0
  64. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +114 -0
  65. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +158 -0
  66. package/dist/repo/shared_libs/play-runtime/profiles.ts +90 -0
  67. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +172 -0
  68. package/dist/repo/shared_libs/play-runtime/protocol.ts +121 -0
  69. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +42 -0
  70. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +33 -0
  71. package/dist/repo/shared_libs/play-runtime/runtime-actions.ts +208 -0
  72. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +1873 -0
  73. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +2 -0
  74. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +201 -0
  75. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +48 -0
  76. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +84 -0
  77. package/dist/repo/shared_libs/play-runtime/scheduler-backend.ts +174 -0
  78. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +147 -0
  79. package/dist/repo/shared_libs/play-runtime/suspension.ts +68 -0
  80. package/dist/repo/shared_libs/play-runtime/tool-batch-executor.ts +146 -0
  81. package/dist/repo/shared_libs/play-runtime/tool-result.ts +387 -0
  82. package/dist/repo/shared_libs/play-runtime/tracing.ts +31 -0
  83. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +75 -0
  84. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +140 -0
  85. package/dist/repo/shared_libs/plays/artifact-transport.ts +14 -0
  86. package/dist/repo/shared_libs/plays/artifact-types.ts +49 -0
  87. package/dist/repo/shared_libs/plays/bundling/index.ts +1346 -0
  88. package/dist/repo/shared_libs/plays/compiler-manifest.ts +186 -0
  89. package/dist/repo/shared_libs/plays/contracts.ts +51 -0
  90. package/dist/repo/shared_libs/plays/dataset.ts +308 -0
  91. package/dist/repo/shared_libs/plays/definition.ts +264 -0
  92. package/dist/repo/shared_libs/plays/file-refs.ts +11 -0
  93. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +206 -0
  94. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +164 -0
  95. package/dist/repo/shared_libs/plays/row-identity.ts +302 -0
  96. package/dist/repo/shared_libs/plays/runtime-validation.ts +415 -0
  97. package/dist/repo/shared_libs/plays/static-pipeline.ts +560 -0
  98. package/dist/repo/shared_libs/temporal/constants.ts +39 -0
  99. package/dist/repo/shared_libs/temporal/preview-config.ts +153 -0
  100. package/package.json +14 -12
@@ -0,0 +1,262 @@
1
+ import {
2
+ getCompiledPipelineSubsteps,
3
+ type PlayStaticPipeline,
4
+ type PlayStaticSubstep,
5
+ } from '../plays/static-pipeline';
6
+
7
+ export const EXECUTION_PLAN_DEFAULTS = {
8
+ inlineRowsLimit: 1_000,
9
+ largeMapChunkSize: 5_000,
10
+ workflowSoftStepBudget: 20_000,
11
+ workflowHardStepBudget: 25_000,
12
+ ingestStepCount: 1,
13
+ finalizationStepCount: 2,
14
+ } as const;
15
+
16
+ export type DatasetHandleReference = {
17
+ kind: 'dataset_handle';
18
+ datasetId: string;
19
+ datasetKind: 'csv' | 'map';
20
+ tableNamespace: string;
21
+ count: number;
22
+ previewRows: Record<string, unknown>[];
23
+ backing: 'inline' | 'neon_sheet' | 'r2_file';
24
+ };
25
+
26
+ export type ExecutionPlanMap = {
27
+ mapName: string;
28
+ tableNamespace: string;
29
+ outputFields: string[];
30
+ waterfallStages: Array<{
31
+ waterfallId: string;
32
+ stageIds: string[];
33
+ }>;
34
+ defaultChunkSize: number;
35
+ stepsPerChunk: number;
36
+ };
37
+
38
+ export type ExecutionPlanChunkPlan = {
39
+ inlineRowsLimit: number;
40
+ defaultLargeMapChunkSize: number;
41
+ softWorkflowStepBudget: number;
42
+ hardWorkflowStepBudget: number;
43
+ estimatedWorkflowSteps: number | null;
44
+ };
45
+
46
+ export type ExecutionPlan = {
47
+ graphHash: string;
48
+ artifactHash: string;
49
+ entrypoint: string;
50
+ assets: Array<{ playPath: string; storageKey: string }>;
51
+ maps: ExecutionPlanMap[];
52
+ toolDeclarations: Array<{ toolId: string; field?: string | null }>;
53
+ chunkPlan: ExecutionPlanChunkPlan;
54
+ persistencePlan: {
55
+ datasetBacking: 'neon_sheet';
56
+ compactWorkflowState: true;
57
+ };
58
+ sdkContract: {
59
+ apiVersion: number;
60
+ artifactVersion: number;
61
+ minRunnerVersion: number;
62
+ };
63
+ };
64
+
65
+ export type BuildExecutionPlanInput = {
66
+ graphHash: string | null | undefined;
67
+ artifactHash: string | null | undefined;
68
+ entrypoint?: string | null;
69
+ assets?: Array<{ playPath?: string | null; storageKey: string }>;
70
+ staticPipeline?: PlayStaticPipeline | null;
71
+ totalRows?: number | null;
72
+ sdkContract?: {
73
+ apiVersion?: number | null;
74
+ artifactVersion?: number | null;
75
+ minRunnerVersion?: number | null;
76
+ } | null;
77
+ };
78
+
79
+ export function buildExecutionPlan(
80
+ input: BuildExecutionPlanInput,
81
+ ): ExecutionPlan {
82
+ const maps = extractPlanMaps(input.staticPipeline ?? null);
83
+ const toolDeclarations = extractToolDeclarations(
84
+ input.staticPipeline ?? null,
85
+ );
86
+ const estimatedWorkflowSteps =
87
+ typeof input.totalRows === 'number' && Number.isFinite(input.totalRows)
88
+ ? estimateWorkflowSteps({
89
+ totalRows: Math.max(0, Math.floor(input.totalRows)),
90
+ maps,
91
+ })
92
+ : null;
93
+
94
+ return {
95
+ graphHash: input.graphHash?.trim() ?? '',
96
+ artifactHash: input.artifactHash?.trim() ?? '',
97
+ entrypoint: input.entrypoint?.trim() || 'default',
98
+ assets: (input.assets ?? [])
99
+ .map((asset) => ({
100
+ playPath: String(asset.playPath ?? '').replace(/^\.\//, ''),
101
+ storageKey: asset.storageKey,
102
+ }))
103
+ .filter(
104
+ (asset) => asset.playPath.length > 0 && asset.storageKey.length > 0,
105
+ ),
106
+ maps,
107
+ toolDeclarations,
108
+ chunkPlan: {
109
+ inlineRowsLimit: EXECUTION_PLAN_DEFAULTS.inlineRowsLimit,
110
+ defaultLargeMapChunkSize: EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
111
+ softWorkflowStepBudget: EXECUTION_PLAN_DEFAULTS.workflowSoftStepBudget,
112
+ hardWorkflowStepBudget: EXECUTION_PLAN_DEFAULTS.workflowHardStepBudget,
113
+ estimatedWorkflowSteps,
114
+ },
115
+ persistencePlan: {
116
+ datasetBacking: 'neon_sheet',
117
+ compactWorkflowState: true,
118
+ },
119
+ sdkContract: {
120
+ apiVersion: input.sdkContract?.apiVersion ?? 1,
121
+ artifactVersion: input.sdkContract?.artifactVersion ?? 1,
122
+ minRunnerVersion: input.sdkContract?.minRunnerVersion ?? 1,
123
+ },
124
+ };
125
+ }
126
+
127
+ export function chooseMapChunkSize(input: {
128
+ totalRows: number;
129
+ mapCount: number;
130
+ stepsPerChunk: number;
131
+ preferredChunkSize?: number | null;
132
+ softWorkflowStepBudget?: number | null;
133
+ }): number {
134
+ const totalRows = Math.max(0, Math.floor(input.totalRows));
135
+ if (totalRows <= EXECUTION_PLAN_DEFAULTS.inlineRowsLimit) {
136
+ return Math.max(1, totalRows || 1);
137
+ }
138
+
139
+ const mapCount = Math.max(1, Math.floor(input.mapCount));
140
+ const stepsPerChunk = Math.max(1, Math.floor(input.stepsPerChunk));
141
+ const softBudget =
142
+ input.softWorkflowStepBudget ??
143
+ EXECUTION_PLAN_DEFAULTS.workflowSoftStepBudget;
144
+ const nonChunkSteps =
145
+ EXECUTION_PLAN_DEFAULTS.ingestStepCount +
146
+ EXECUTION_PLAN_DEFAULTS.finalizationStepCount;
147
+ const maxChunksAcrossMaps = Math.max(
148
+ mapCount,
149
+ Math.floor((softBudget - nonChunkSteps) / stepsPerChunk),
150
+ );
151
+ const maxChunksPerMap = Math.max(
152
+ 1,
153
+ Math.floor(maxChunksAcrossMaps / mapCount),
154
+ );
155
+ const minimumSurvivalChunkSize = Math.max(
156
+ 1,
157
+ Math.ceil(totalRows / maxChunksPerMap),
158
+ );
159
+ const preferred = Math.max(
160
+ 1,
161
+ Math.floor(
162
+ input.preferredChunkSize ?? EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
163
+ ),
164
+ );
165
+ return Math.max(preferred, minimumSurvivalChunkSize);
166
+ }
167
+
168
+ export function deterministicMapChunkStepName(input: {
169
+ mapName: string;
170
+ chunkIndex: number;
171
+ phase?: 'prepare' | 'execute' | 'persist' | string;
172
+ }): string {
173
+ const phase = input.phase?.trim() || 'execute';
174
+ return `map:${input.mapName}:chunk:${String(input.chunkIndex).padStart(4, '0')}:${phase}`;
175
+ }
176
+
177
+ function extractPlanMaps(
178
+ pipeline: PlayStaticPipeline | null,
179
+ ): ExecutionPlanMap[] {
180
+ if (!pipeline) return [];
181
+ const substeps = getCompiledPipelineSubsteps(pipeline);
182
+ const fallbackWaterfalls = substeps.filter(
183
+ (substep): substep is Extract<PlayStaticSubstep, { type: 'waterfall' }> =>
184
+ substep.type === 'waterfall',
185
+ );
186
+ return substeps
187
+ .filter(
188
+ (substep): substep is Extract<PlayStaticSubstep, { type: 'map' }> =>
189
+ substep.type === 'map',
190
+ )
191
+ .map((mapSubstep) => {
192
+ const waterfalls = fallbackWaterfalls.filter((waterfall) => {
193
+ if (!mapSubstep.waterfallIds?.length) return true;
194
+ return (
195
+ (waterfall.id && mapSubstep.waterfallIds.includes(waterfall.id)) ||
196
+ mapSubstep.waterfallIds.includes(waterfall.field)
197
+ );
198
+ });
199
+ const waterfallStages = waterfalls.map((waterfall) => ({
200
+ waterfallId: waterfall.id ?? waterfall.field,
201
+ stageIds: waterfall.steps?.map((step) => step.id) ?? [],
202
+ }));
203
+ return {
204
+ mapName: mapSubstep.name ?? mapSubstep.field,
205
+ tableNamespace: mapSubstep.tableNamespace ?? mapSubstep.field,
206
+ outputFields: mapSubstep.outputFields ?? [],
207
+ waterfallStages,
208
+ defaultChunkSize: EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
209
+ stepsPerChunk: 1,
210
+ };
211
+ });
212
+ }
213
+
214
+ function extractToolDeclarations(
215
+ pipeline: PlayStaticPipeline | null,
216
+ ): ExecutionPlan['toolDeclarations'] {
217
+ if (!pipeline) return [];
218
+ const seen = new Set<string>();
219
+ const declarations: ExecutionPlan['toolDeclarations'] = [];
220
+ for (const substep of getCompiledPipelineSubsteps(pipeline)) {
221
+ if (substep.type === 'tool') {
222
+ const key = `${substep.toolId}:${substep.field}`;
223
+ if (!seen.has(key)) {
224
+ seen.add(key);
225
+ declarations.push({ toolId: substep.toolId, field: substep.field });
226
+ }
227
+ continue;
228
+ }
229
+ if (substep.type === 'waterfall') {
230
+ for (const step of substep.steps ?? []) {
231
+ if (!step.toolId) continue;
232
+ const key = `${step.toolId}:${substep.field}`;
233
+ if (!seen.has(key)) {
234
+ seen.add(key);
235
+ declarations.push({ toolId: step.toolId, field: substep.field });
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return declarations;
241
+ }
242
+
243
+ function estimateWorkflowSteps(input: {
244
+ totalRows: number;
245
+ maps: ExecutionPlanMap[];
246
+ }): number {
247
+ const maps = input.maps.length > 0 ? input.maps : [];
248
+ const chunkSteps = maps.reduce((sum, map) => {
249
+ const chunkSize = chooseMapChunkSize({
250
+ totalRows: input.totalRows,
251
+ mapCount: maps.length,
252
+ stepsPerChunk: map.stepsPerChunk,
253
+ preferredChunkSize: map.defaultChunkSize,
254
+ });
255
+ return sum + Math.ceil(input.totalRows / chunkSize) * map.stepsPerChunk;
256
+ }, 0);
257
+ return (
258
+ EXECUTION_PLAN_DEFAULTS.ingestStepCount +
259
+ chunkSteps +
260
+ EXECUTION_PLAN_DEFAULTS.finalizationStepCount
261
+ );
262
+ }
@@ -0,0 +1,214 @@
1
+ export type PlayLiveEventSource =
2
+ | 'worker'
3
+ | 'temporal'
4
+ | 'convex'
5
+ | 'play'
6
+ | 'system';
7
+ export type PlayRunTimelineEventType =
8
+ | 'play.run.status'
9
+ | 'play.run.snapshot'
10
+ | 'play.step.status'
11
+ | 'play.step.progress'
12
+ | 'play.run.health'
13
+ | 'play.run.log'
14
+ | 'play.sheet.summary'
15
+ | 'play.sheet.delta';
16
+
17
+ export type PlayRunTimelineLogEntry = {
18
+ kind: 'log';
19
+ at: string;
20
+ source: PlayLiveEventSource;
21
+ message: string;
22
+ };
23
+
24
+ export type PlayRunTimelineEventEntry = {
25
+ kind: 'event';
26
+ at: string;
27
+ eventType: PlayRunTimelineEventType;
28
+ summary: string;
29
+ };
30
+
31
+ export type PlayRunTimelineEntry =
32
+ | PlayRunTimelineLogEntry
33
+ | PlayRunTimelineEventEntry;
34
+
35
+ export function makePlayRunTimelineLogEntry(input: {
36
+ at?: string;
37
+ source: PlayLiveEventSource;
38
+ message: string;
39
+ }): PlayRunTimelineLogEntry {
40
+ return {
41
+ kind: 'log',
42
+ at: input.at ?? new Date().toISOString(),
43
+ source: input.source,
44
+ message: input.message,
45
+ };
46
+ }
47
+
48
+ export function makePlayRunTimelineEventEntry(input: {
49
+ at?: string;
50
+ eventType: PlayRunTimelineEventType;
51
+ summary: string;
52
+ }): PlayRunTimelineEventEntry {
53
+ return {
54
+ kind: 'event',
55
+ at: input.at ?? new Date().toISOString(),
56
+ eventType: input.eventType,
57
+ summary: input.summary,
58
+ };
59
+ }
60
+
61
+ export type PlayRuntimeTimingWindow = {
62
+ startedAt?: number | null;
63
+ completedAt?: number | null;
64
+ updatedAt?: number | null;
65
+ };
66
+
67
+ export type PlayStepStatusEventPayload = {
68
+ runId: string;
69
+ stepId: string;
70
+ status: 'running' | 'completed' | 'failed' | 'skipped';
71
+ label?: string;
72
+ artifactTableNamespace?: string | null;
73
+ } & PlayRuntimeTimingWindow;
74
+
75
+ export type PlayStepProgressEventPayload = {
76
+ runId: string;
77
+ stepId: string;
78
+ completed?: number;
79
+ total?: number;
80
+ failed?: number;
81
+ message?: string;
82
+ artifactTableNamespace?: string | null;
83
+ } & PlayRuntimeTimingWindow;
84
+
85
+ export type PlayRunHealthEventPayload = {
86
+ runId: string;
87
+ state: 'healthy' | 'degraded';
88
+ reasons: string[];
89
+ message: string;
90
+ taskQueue: string | null;
91
+ workflowPollerCount?: number;
92
+ activityPollerCount?: number;
93
+ lastHeartbeatAgeMs?: number | null;
94
+ };
95
+
96
+ export type PlayRunLogEventPayload = {
97
+ runId: string;
98
+ lines: string[];
99
+ source: PlayLiveEventSource;
100
+ };
101
+
102
+ export type PlaySheetStats = {
103
+ total: number;
104
+ queued: number;
105
+ running: number;
106
+ completed: number;
107
+ failed: number;
108
+ };
109
+
110
+ export type PlaySheetColumnStats = {
111
+ queued: number;
112
+ running: number;
113
+ completed: number;
114
+ failed: number;
115
+ cached: number;
116
+ };
117
+
118
+ export type PlaySheetSummaryPayload = {
119
+ runId: string;
120
+ tableNamespace: string;
121
+ deltaCursor: number;
122
+ summary: {
123
+ stats: PlaySheetStats;
124
+ columns: Record<string, PlaySheetColumnStats>;
125
+ } | null;
126
+ previewRows: Array<{
127
+ key: string;
128
+ status: string;
129
+ data: Record<string, unknown>;
130
+ cellMeta: Record<string, unknown>;
131
+ }>;
132
+ };
133
+
134
+ export type PlaySheetDeltaEventPayload = {
135
+ runId: string;
136
+ tableNamespace: string;
137
+ nextCursor: number;
138
+ updates: Array<{
139
+ key: string;
140
+ status: string;
141
+ data: Record<string, unknown>;
142
+ cellMeta: Record<string, unknown>;
143
+ inputIndex?: number;
144
+ runId?: string;
145
+ error?: string;
146
+ stage?: string;
147
+ provider?: string;
148
+ seq?: number;
149
+ createdAt: string;
150
+ updatedAt: string;
151
+ }>;
152
+ };
153
+
154
+ export function resolveTimingWindow(input: {
155
+ startedAt?: number | null;
156
+ completedAt?: number | null;
157
+ updatedAt?: number | null;
158
+ }): PlayRuntimeTimingWindow {
159
+ const startedAt =
160
+ typeof input.startedAt === 'number' && Number.isFinite(input.startedAt)
161
+ ? input.startedAt
162
+ : null;
163
+ const completedAt =
164
+ typeof input.completedAt === 'number' && Number.isFinite(input.completedAt)
165
+ ? input.completedAt
166
+ : null;
167
+ const updatedAt =
168
+ typeof input.updatedAt === 'number' && Number.isFinite(input.updatedAt)
169
+ ? input.updatedAt
170
+ : null;
171
+
172
+ return {
173
+ startedAt,
174
+ completedAt,
175
+ updatedAt,
176
+ };
177
+ }
178
+
179
+ export function getTimingDurationMs(input: {
180
+ startedAt?: number | null;
181
+ completedAt?: number | null;
182
+ }): number | null {
183
+ return typeof input.startedAt === 'number' &&
184
+ Number.isFinite(input.startedAt) &&
185
+ typeof input.completedAt === 'number' &&
186
+ Number.isFinite(input.completedAt)
187
+ ? Math.max(0, input.completedAt - input.startedAt)
188
+ : null;
189
+ }
190
+
191
+ export function getTimingActiveDurationMs(input: {
192
+ startedAt?: number | null;
193
+ completedAt?: number | null;
194
+ updatedAt?: number | null;
195
+ now?: number;
196
+ }): number | null {
197
+ if (
198
+ typeof input.startedAt !== 'number' ||
199
+ !Number.isFinite(input.startedAt)
200
+ ) {
201
+ return null;
202
+ }
203
+ if (
204
+ typeof input.completedAt === 'number' &&
205
+ Number.isFinite(input.completedAt)
206
+ ) {
207
+ return Math.max(0, input.completedAt - input.startedAt);
208
+ }
209
+ const referenceAt =
210
+ typeof input.updatedAt === 'number' && Number.isFinite(input.updatedAt)
211
+ ? input.updatedAt
212
+ : input.now ?? Date.now();
213
+ return Math.max(0, referenceAt - input.startedAt);
214
+ }
@@ -0,0 +1,50 @@
1
+ export type PlayRunLiveStatus =
2
+ | 'queued'
3
+ | 'running'
4
+ | 'completed'
5
+ | 'failed'
6
+ | 'cancelled'
7
+ | 'terminated'
8
+ | 'timed_out'
9
+ | 'unknown';
10
+
11
+ export type PlayVisualNodeStatus =
12
+ | 'idle'
13
+ | 'running'
14
+ | 'completed'
15
+ | 'failed'
16
+ | 'skipped';
17
+
18
+ export type PlayStepLiveStatus = Exclude<PlayVisualNodeStatus, 'idle'>;
19
+
20
+ export type PlayVisualNodeProgressSnapshot = {
21
+ completed?: number;
22
+ total?: number;
23
+ failed?: number;
24
+ message?: string;
25
+ updatedAt?: number | null;
26
+ startedAt?: number | null;
27
+ completedAt?: number | null;
28
+ artifactTableNamespace?: string | null;
29
+ };
30
+
31
+ export type PlayVisualNodeStateSnapshot = {
32
+ nodeId: string;
33
+ status: PlayVisualNodeStatus;
34
+ artifactTableNamespace?: string | null;
35
+ progress?: PlayVisualNodeProgressSnapshot | null;
36
+ startedAt?: number | null;
37
+ completedAt?: number | null;
38
+ updatedAt?: number | null;
39
+ };
40
+
41
+ export type PlayRunLiveSnapshot = {
42
+ runId: string;
43
+ status: PlayRunLiveStatus;
44
+ updatedAt: number | null;
45
+ logs: string[];
46
+ activeArtifactTableNamespace: string | null;
47
+ resultTableNamespace: string | null;
48
+ nodeStates: PlayVisualNodeStateSnapshot[];
49
+ activeNodeId: string | null;
50
+ };
@@ -0,0 +1,114 @@
1
+ import type {
2
+ MapExecutionFrame,
3
+ MapExecutionScope,
4
+ PlayCheckpoint,
5
+ PlayExecutionEvent,
6
+ } from './ctx-types';
7
+
8
+ export function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
9
+ return {
10
+ ...frame,
11
+ completedRowKeys: [...frame.completedRowKeys],
12
+ pendingRowKeys: [...frame.pendingRowKeys],
13
+ };
14
+ }
15
+
16
+ export class MapExecutionFrameStore {
17
+ constructor(
18
+ private readonly checkpoint: PlayCheckpoint,
19
+ private readonly emitExecutionEvent: (event: PlayExecutionEvent) => void,
20
+ ) {}
21
+
22
+ set(frame: MapExecutionFrame): void {
23
+ this.checkpoint.mapFrames = {
24
+ ...(this.checkpoint.mapFrames ?? {}),
25
+ [frame.mapInvocationId]: cloneMapFrame(frame),
26
+ };
27
+ }
28
+
29
+ start(input: {
30
+ scope: MapExecutionScope;
31
+ totalRows: number;
32
+ completedRowKeys: readonly string[];
33
+ pendingRowKeys: readonly string[];
34
+ }): void {
35
+ const now = Date.now();
36
+ this.set({
37
+ mapInvocationId: input.scope.mapInvocationId,
38
+ mapNodeId: input.scope.mapNodeId ?? null,
39
+ logicalNamespace: input.scope.logicalNamespace,
40
+ artifactTableNamespace: input.scope.artifactTableNamespace,
41
+ status: 'running',
42
+ totalRows: input.totalRows,
43
+ completedRowKeys: [...input.completedRowKeys],
44
+ pendingRowKeys: [...input.pendingRowKeys],
45
+ startedAt: now,
46
+ updatedAt: now,
47
+ });
48
+ this.emitExecutionEvent({
49
+ type: 'map.started',
50
+ mapInvocationId: input.scope.mapInvocationId,
51
+ mapNodeId: input.scope.mapNodeId ?? null,
52
+ logicalNamespace: input.scope.logicalNamespace,
53
+ artifactTableNamespace: input.scope.artifactTableNamespace,
54
+ totalRows: input.totalRows,
55
+ completedRows: input.completedRowKeys.length,
56
+ pendingRows: input.pendingRowKeys.length,
57
+ at: Date.now(),
58
+ });
59
+ }
60
+
61
+ updateProgress(input: {
62
+ scope: MapExecutionScope;
63
+ totalRows: number;
64
+ status?: MapExecutionFrame['status'];
65
+ completedRowKey?: string | null;
66
+ pendingRowKey?: string | null;
67
+ activeBoundaryId?: string | null;
68
+ emitEventType?: PlayExecutionEvent['type'];
69
+ }): void {
70
+ const existing =
71
+ this.checkpoint.mapFrames?.[input.scope.mapInvocationId] ?? null;
72
+ if (!existing) {
73
+ return;
74
+ }
75
+ const completedRowKeys = new Set(existing.completedRowKeys);
76
+ const pendingRowKeys = new Set(existing.pendingRowKeys);
77
+ if (input.completedRowKey?.trim()) {
78
+ completedRowKeys.add(input.completedRowKey.trim());
79
+ pendingRowKeys.delete(input.completedRowKey.trim());
80
+ }
81
+ if (input.pendingRowKey?.trim()) {
82
+ pendingRowKeys.add(input.pendingRowKey.trim());
83
+ }
84
+ const nextFrame: MapExecutionFrame = {
85
+ ...existing,
86
+ status: input.status ?? existing.status,
87
+ completedRowKeys: [...completedRowKeys],
88
+ pendingRowKeys: [...pendingRowKeys],
89
+ ...(input.activeBoundaryId !== undefined
90
+ ? { activeBoundaryId: input.activeBoundaryId }
91
+ : {}),
92
+ updatedAt: Date.now(),
93
+ };
94
+ this.set(nextFrame);
95
+ if (input.emitEventType) {
96
+ this.emitExecutionEvent({
97
+ type: input.emitEventType,
98
+ mapInvocationId: input.scope.mapInvocationId,
99
+ mapNodeId: input.scope.mapNodeId ?? null,
100
+ logicalNamespace: input.scope.logicalNamespace,
101
+ artifactTableNamespace: input.scope.artifactTableNamespace,
102
+ completedRows: nextFrame.completedRowKeys.length,
103
+ failedRows: Math.max(
104
+ 0,
105
+ input.totalRows -
106
+ nextFrame.completedRowKeys.length -
107
+ nextFrame.pendingRowKeys.length,
108
+ ),
109
+ totalRows: input.totalRows,
110
+ at: Date.now(),
111
+ } as PlayExecutionEvent);
112
+ }
113
+ }
114
+ }