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,264 @@
1
+ export type PlayPrimitive = string | number | boolean | null;
2
+
3
+ export type PlayValueTemplate =
4
+ | PlayPrimitive
5
+ | { $ref: string }
6
+ | { [key: string]: PlayValueTemplate }
7
+ | PlayValueTemplate[];
8
+
9
+ export type SerializedPlayJavascript = {
10
+ kind: 'serialized_javascript';
11
+ source: string;
12
+ displayName?: string;
13
+ };
14
+
15
+ export type PlayJavascriptExecutorContext = {
16
+ row: Record<string, unknown>;
17
+ steps: Record<string, unknown>;
18
+ input: Record<string, unknown>;
19
+ index: number;
20
+ };
21
+
22
+ export type PlayJavascriptExecutor<TResult = unknown> = (
23
+ context: PlayJavascriptExecutorContext,
24
+ ) => TResult | Promise<TResult>;
25
+
26
+ export type PlayStructuredStep =
27
+ | {
28
+ type: 'tool';
29
+ alias: string;
30
+ toolId: string;
31
+ input: Record<string, PlayValueTemplate>;
32
+ description?: string;
33
+ }
34
+ | {
35
+ type: 'waterfall';
36
+ alias: string;
37
+ tool: string;
38
+ input: Record<string, PlayValueTemplate>;
39
+ providers?: string[];
40
+ description?: string;
41
+ }
42
+ | {
43
+ type: 'run_javascript';
44
+ alias: string;
45
+ execute: SerializedPlayJavascript;
46
+ input?: Record<string, PlayValueTemplate>;
47
+ description?: string;
48
+ displayAs?: 'run_javascript';
49
+ };
50
+
51
+ export type PlayStructuredDefinition = {
52
+ version: 1;
53
+ map: {
54
+ key: string;
55
+ steps: PlayStructuredStep[];
56
+ };
57
+ result?: {
58
+ mode?: 'rows';
59
+ };
60
+ };
61
+
62
+ export function serializeExecutableJs<TResult = unknown>(
63
+ execute: PlayJavascriptExecutor<TResult>,
64
+ displayName?: string,
65
+ ): SerializedPlayJavascript {
66
+ const source = execute.toString().trim();
67
+ if (!source) {
68
+ throw new Error('Executable JavaScript function source must be non-empty.');
69
+ }
70
+ return {
71
+ kind: 'serialized_javascript',
72
+ source,
73
+ displayName,
74
+ };
75
+ }
76
+
77
+ export function runJavascriptStep<TResult = unknown>(input: {
78
+ alias: string;
79
+ execute: PlayJavascriptExecutor<TResult>;
80
+ description?: string;
81
+ input?: Record<string, PlayValueTemplate>;
82
+ }): Extract<PlayStructuredStep, { type: 'run_javascript' }> {
83
+ return {
84
+ type: 'run_javascript',
85
+ alias: input.alias,
86
+ execute: serializeExecutableJs(input.execute, input.alias),
87
+ ...(input.description ? { description: input.description } : {}),
88
+ ...(input.input ? { input: input.input } : {}),
89
+ displayAs: 'run_javascript',
90
+ };
91
+ }
92
+
93
+ export function definePlayDefinition(
94
+ definition: PlayStructuredDefinition,
95
+ ): PlayStructuredDefinition {
96
+ return definition;
97
+ }
98
+
99
+ function formatTemplate(template: PlayValueTemplate): string {
100
+ if (
101
+ template === null ||
102
+ typeof template === 'string' ||
103
+ typeof template === 'number' ||
104
+ typeof template === 'boolean'
105
+ ) {
106
+ return JSON.stringify(template);
107
+ }
108
+ if (Array.isArray(template)) {
109
+ return `[${template.map((value) => formatTemplate(value)).join(', ')}]`;
110
+ }
111
+ if ('$ref' in template && typeof template.$ref === 'string') {
112
+ return `ref(${JSON.stringify(template.$ref)})`;
113
+ }
114
+ return `{ ${Object.entries(template)
115
+ .map(([key, value]) => `${JSON.stringify(key)}: ${formatTemplate(value)}`)
116
+ .join(', ')} }`;
117
+ }
118
+
119
+ export function renderPlayDefinitionSource(
120
+ definition: PlayStructuredDefinition,
121
+ ): string {
122
+ const lines: string[] = [];
123
+ lines.push('definePlayDefinition({');
124
+ lines.push(' version: 1,');
125
+ lines.push(' map: {');
126
+ lines.push(` key: ${JSON.stringify(definition.map.key)},`);
127
+ lines.push(' steps: [');
128
+ for (const step of definition.map.steps) {
129
+ if (step.type === 'tool') {
130
+ lines.push(' {');
131
+ lines.push(' type: "tool",');
132
+ lines.push(` alias: ${JSON.stringify(step.alias)},`);
133
+ lines.push(` toolId: ${JSON.stringify(step.toolId)},`);
134
+ lines.push(` input: ${formatTemplate(step.input)},`);
135
+ if (step.description) {
136
+ lines.push(` description: ${JSON.stringify(step.description)},`);
137
+ }
138
+ lines.push(' },');
139
+ continue;
140
+ }
141
+ if (step.type === 'waterfall') {
142
+ lines.push(' {');
143
+ lines.push(' type: "waterfall",');
144
+ lines.push(` alias: ${JSON.stringify(step.alias)},`);
145
+ lines.push(` tool: ${JSON.stringify(step.tool)},`);
146
+ lines.push(` input: ${formatTemplate(step.input)},`);
147
+ if (step.providers?.length) {
148
+ lines.push(` providers: ${JSON.stringify(step.providers)},`);
149
+ }
150
+ if (step.description) {
151
+ lines.push(` description: ${JSON.stringify(step.description)},`);
152
+ }
153
+ lines.push(' },');
154
+ continue;
155
+ }
156
+ lines.push(` runJavascriptStep({
157
+ alias: ${JSON.stringify(step.alias)},
158
+ execute: ${step.execute.source},
159
+ ${step.input ? `input: ${formatTemplate(step.input)},` : ''}
160
+ }),`);
161
+ }
162
+ lines.push(' ],');
163
+ lines.push(' },');
164
+ lines.push('});');
165
+ return lines.join('\n');
166
+ }
167
+
168
+ export function validatePlayStructuredDefinition(
169
+ definition: unknown,
170
+ ): { valid: boolean; errors: string[] } {
171
+ const errors: string[] = [];
172
+ if (!definition || typeof definition !== 'object') {
173
+ return { valid: false, errors: ['Definition must be an object.'] };
174
+ }
175
+ const candidate = definition as Partial<PlayStructuredDefinition>;
176
+ if (candidate.version !== 1) {
177
+ errors.push('Definition version must be 1.');
178
+ }
179
+ if (typeof candidate.map?.key !== 'string' || !candidate.map.key.trim()) {
180
+ errors.push('Definition map.key must be a non-empty string.');
181
+ }
182
+ if (!Array.isArray(candidate.map?.steps) || candidate.map.steps.length === 0) {
183
+ errors.push('Definition must contain at least one step.');
184
+ }
185
+ for (const [index, step] of (candidate.map?.steps ?? []).entries()) {
186
+ if (!step || typeof step !== 'object') {
187
+ errors.push(`Step ${index} must be an object.`);
188
+ continue;
189
+ }
190
+ const typedStep = step as PlayStructuredStep;
191
+ if (!typedStep.alias?.trim()) {
192
+ errors.push(`Step ${index} requires a non-empty alias.`);
193
+ }
194
+ if (typedStep.type === 'tool' && !typedStep.toolId?.trim()) {
195
+ errors.push(`Tool step ${typedStep.alias || index} requires toolId.`);
196
+ }
197
+ if (typedStep.type === 'waterfall' && !typedStep.tool?.trim()) {
198
+ errors.push(`Waterfall step ${typedStep.alias || index} requires tool.`);
199
+ }
200
+ if (typedStep.type === 'run_javascript') {
201
+ if (!typedStep.execute?.source?.trim()) {
202
+ errors.push(`run_javascript step ${typedStep.alias || index} requires executable source.`);
203
+ continue;
204
+ }
205
+ try {
206
+ new Function(`return (${typedStep.execute.source});`);
207
+ } catch (error) {
208
+ errors.push(
209
+ `run_javascript step ${typedStep.alias || index} parse error: ${
210
+ error instanceof Error ? error.message : String(error)
211
+ }`,
212
+ );
213
+ }
214
+ }
215
+ }
216
+ return { valid: errors.length === 0, errors };
217
+ }
218
+
219
+ function getPathValue(
220
+ value: unknown,
221
+ path: string,
222
+ ): unknown {
223
+ if (!path) return value;
224
+ const normalized = path.replace(/^\$?/, '');
225
+ const parts = normalized.split('.').filter(Boolean);
226
+ let current = value;
227
+ for (const part of parts) {
228
+ if (current == null || typeof current !== 'object') return undefined;
229
+ current = (current as Record<string, unknown>)[part];
230
+ }
231
+ return current;
232
+ }
233
+
234
+ export function resolvePlayValueTemplate(
235
+ template: PlayValueTemplate,
236
+ context: PlayJavascriptExecutorContext,
237
+ ): unknown {
238
+ if (
239
+ template === null ||
240
+ typeof template === 'string' ||
241
+ typeof template === 'number' ||
242
+ typeof template === 'boolean'
243
+ ) {
244
+ return template;
245
+ }
246
+ if (Array.isArray(template)) {
247
+ return template.map((item) => resolvePlayValueTemplate(item, context));
248
+ }
249
+ if ('$ref' in template && typeof template.$ref === 'string') {
250
+ const [root, ...rest] = template.$ref.split('.');
251
+ const path = rest.join('.');
252
+ if (root === 'row') return getPathValue(context.row, path);
253
+ if (root === 'steps') return getPathValue(context.steps, path);
254
+ if (root === 'input') return getPathValue(context.input, path);
255
+ if (root === 'index') return context.index;
256
+ return undefined;
257
+ }
258
+ return Object.fromEntries(
259
+ Object.entries(template).map(([key, value]) => [
260
+ key,
261
+ resolvePlayValueTemplate(value, context),
262
+ ]),
263
+ );
264
+ }
@@ -0,0 +1,11 @@
1
+ export interface PlayR2FileRef {
2
+ storageKind: 'r2';
3
+ storageKey: string;
4
+ logicalPath: string;
5
+ fileName: string;
6
+ contentHash: string;
7
+ contentType: string;
8
+ bytes: number;
9
+ }
10
+
11
+ export type PlayExecutionFileRef = PlayR2FileRef;
@@ -0,0 +1,206 @@
1
+ const DEFAULT_SUGGESTED_MAX_PARALLELISM = 50;
2
+ const MIN_CONCURRENCY_WAIT_MS = 10;
3
+
4
+ type BucketState = {
5
+ windowStartedAt: number;
6
+ startedInWindow: number;
7
+ activeCount: number;
8
+ };
9
+
10
+ export interface PlayQueueHint {
11
+ bucketId: string;
12
+ provider: string;
13
+ operation: string;
14
+ ruleId: string;
15
+ requestsPerWindow: number;
16
+ windowMs: number;
17
+ maxConcurrency: number | null;
18
+ }
19
+
20
+ type SchedulerOptions = {
21
+ now?: () => number;
22
+ sleep?: (ms: number) => Promise<void>;
23
+ maxSuggestedParallelism?: number;
24
+ getQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
25
+ };
26
+
27
+ export async function getSuggestedPlayToolParallelism(
28
+ toolId: string,
29
+ fallback: number,
30
+ maxSuggestedParallelism = DEFAULT_SUGGESTED_MAX_PARALLELISM,
31
+ getQueueHints: (toolId: string) => Promise<readonly PlayQueueHint[]> = async () => [],
32
+ ): Promise<number> {
33
+ const hints = await getQueueHints(toolId);
34
+ if (hints.length === 0) {
35
+ return fallback;
36
+ }
37
+
38
+ const limits = hints.flatMap((hint) =>
39
+ hint.maxConcurrency != null
40
+ ? [hint.requestsPerWindow, hint.maxConcurrency]
41
+ : [hint.requestsPerWindow],
42
+ );
43
+
44
+ return Math.max(1, Math.min(maxSuggestedParallelism, ...limits));
45
+ }
46
+
47
+ export class PlayRateLimitScheduler {
48
+ private readonly bucketStates = new Map<string, BucketState>();
49
+ private lock: Promise<void> = Promise.resolve();
50
+ private readonly now;
51
+ private readonly sleep;
52
+ private readonly maxSuggestedParallelism;
53
+ private readonly getQueueHints;
54
+
55
+ constructor(options: SchedulerOptions = {}) {
56
+ this.now = options.now ?? (() => Date.now());
57
+ this.sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
58
+ this.maxSuggestedParallelism =
59
+ options.maxSuggestedParallelism ?? DEFAULT_SUGGESTED_MAX_PARALLELISM;
60
+ this.getQueueHints = options.getQueueHints ?? (async () => []);
61
+ }
62
+
63
+ async getSuggestedParallelism(toolId: string, fallback: number): Promise<number> {
64
+ return await getSuggestedPlayToolParallelism(
65
+ toolId,
66
+ fallback,
67
+ this.maxSuggestedParallelism,
68
+ this.getQueueHints,
69
+ );
70
+ }
71
+
72
+ async run<T>(toolId: string, task: () => Promise<T>): Promise<T> {
73
+ const hints = await this.getQueueHints(toolId);
74
+ if (hints.length === 0) {
75
+ return await task();
76
+ }
77
+
78
+ await this.acquire(hints);
79
+ try {
80
+ return await task();
81
+ } finally {
82
+ await this.release(hints);
83
+ }
84
+ }
85
+
86
+ private async acquire(hints: readonly PlayQueueHint[]): Promise<void> {
87
+ const orderedHints = [...hints].sort((left, right) =>
88
+ left.bucketId.localeCompare(right.bucketId),
89
+ );
90
+
91
+ while (true) {
92
+ const decision = await this.withLock(() => {
93
+ const now = this.now();
94
+ let waitMs = 0;
95
+
96
+ for (const hint of orderedHints) {
97
+ const state = this.getBucketState(hint.bucketId, now);
98
+ this.resetExpiredWindow(state, hint.windowMs, now);
99
+
100
+ if (state.startedInWindow >= hint.requestsPerWindow) {
101
+ waitMs = Math.max(
102
+ waitMs,
103
+ state.windowStartedAt + hint.windowMs - now,
104
+ );
105
+ }
106
+
107
+ if (
108
+ hint.maxConcurrency != null &&
109
+ state.activeCount >= hint.maxConcurrency
110
+ ) {
111
+ waitMs = Math.max(waitMs, MIN_CONCURRENCY_WAIT_MS);
112
+ }
113
+ }
114
+
115
+ if (waitMs > 0) {
116
+ return { acquired: false, waitMs: Math.max(1, waitMs) } as const;
117
+ }
118
+
119
+ for (const hint of orderedHints) {
120
+ const state = this.getBucketState(hint.bucketId, now);
121
+ state.startedInWindow += 1;
122
+ if (hint.maxConcurrency != null) {
123
+ state.activeCount += 1;
124
+ }
125
+ }
126
+
127
+ return { acquired: true, waitMs: 0 } as const;
128
+ });
129
+
130
+ if (decision.acquired) {
131
+ return;
132
+ }
133
+
134
+ await this.sleep(decision.waitMs);
135
+ }
136
+ }
137
+
138
+ private async release(hints: readonly PlayQueueHint[]): Promise<void> {
139
+ await this.withLock(() => {
140
+ for (const hint of hints) {
141
+ if (hint.maxConcurrency == null) {
142
+ continue;
143
+ }
144
+ const state = this.bucketStates.get(hint.bucketId);
145
+ if (!state || state.activeCount <= 0) {
146
+ continue;
147
+ }
148
+ state.activeCount -= 1;
149
+ }
150
+ });
151
+ }
152
+
153
+ private getBucketState(bucketId: string, now: number): BucketState {
154
+ const existing = this.bucketStates.get(bucketId);
155
+ if (existing) {
156
+ return existing;
157
+ }
158
+
159
+ const created: BucketState = {
160
+ windowStartedAt: now,
161
+ startedInWindow: 0,
162
+ activeCount: 0,
163
+ };
164
+ this.bucketStates.set(bucketId, created);
165
+ return created;
166
+ }
167
+
168
+ private resetExpiredWindow(
169
+ state: BucketState,
170
+ windowMs: number,
171
+ now: number,
172
+ ): void {
173
+ if (windowMs <= 0) {
174
+ state.windowStartedAt = now;
175
+ state.startedInWindow = 0;
176
+ return;
177
+ }
178
+
179
+ if (now - state.windowStartedAt < windowMs) {
180
+ return;
181
+ }
182
+
183
+ const windowsElapsed = Math.floor((now - state.windowStartedAt) / windowMs);
184
+ state.windowStartedAt += windowsElapsed * windowMs;
185
+ state.startedInWindow = 0;
186
+
187
+ if (now - state.windowStartedAt >= windowMs) {
188
+ state.windowStartedAt = now;
189
+ }
190
+ }
191
+
192
+ private async withLock<T>(fn: () => T | Promise<T>): Promise<T> {
193
+ const previous = this.lock;
194
+ let releaseLock!: () => void;
195
+ this.lock = new Promise<void>((resolve) => {
196
+ releaseLock = resolve;
197
+ });
198
+
199
+ await previous;
200
+ try {
201
+ return await fn();
202
+ } finally {
203
+ releaseLock();
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,164 @@
1
+ import type {
2
+ PlaySheetContract,
3
+ PlayStaticPipeline,
4
+ PlayStaticSubstep,
5
+ } from './static-pipeline';
6
+ import { compileSheetContract } from './static-pipeline';
7
+
8
+ export type ResolvablePlayStaticShape = {
9
+ staticPipeline?: PlayStaticPipeline | null;
10
+ };
11
+
12
+ export type ResolveStaticPipelineInput = {
13
+ rootPlayId: string;
14
+ root: ResolvablePlayStaticShape;
15
+ resolveReferencedPlay: (playId: string) => Promise<ResolvablePlayStaticShape | null>;
16
+ maxDepth?: number;
17
+ };
18
+
19
+ function clonePipeline(pipeline: PlayStaticPipeline): PlayStaticPipeline {
20
+ return {
21
+ ...pipeline,
22
+ inputFields: pipeline.inputFields ? [...pipeline.inputFields] : undefined,
23
+ fields: [...pipeline.fields],
24
+ stages: (pipeline.stages ?? []).map((stage) => ({ ...stage })),
25
+ substeps: pipeline.substeps.map((substep) => ({ ...substep })),
26
+ sheetContract: cloneSheetContract(pipeline.sheetContract),
27
+ sheetContractErrors: undefined,
28
+ };
29
+ }
30
+
31
+ function cloneSheetContract(contract: PlaySheetContract | null | undefined) {
32
+ if (!contract) return contract;
33
+ return {
34
+ ...contract,
35
+ columns: contract.columns.map((column) => ({ ...column })),
36
+ };
37
+ }
38
+
39
+ function materializePipeline(shape: ResolvablePlayStaticShape | null): PlayStaticPipeline | null {
40
+ if (!shape) return null;
41
+ if (shape.staticPipeline) {
42
+ return clonePipeline(shape.staticPipeline);
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function annotateSubstep(
48
+ substep: PlayStaticSubstep,
49
+ input: { callDepth: number; callPath: string[] },
50
+ ): PlayStaticSubstep {
51
+ return {
52
+ ...substep,
53
+ callDepth: input.callDepth,
54
+ callPath: input.callPath,
55
+ };
56
+ }
57
+
58
+ async function resolvePipelineSubsteps(input: {
59
+ steps: PlayStaticSubstep[];
60
+ resolveReferencedPlay: (playId: string) => Promise<ResolvablePlayStaticShape | null>;
61
+ stack: string[];
62
+ maxDepth: number;
63
+ }): Promise<PlayStaticSubstep[]> {
64
+ const { steps, resolveReferencedPlay, stack, maxDepth } = input;
65
+ const callDepth = Math.max(0, stack.length - 1);
66
+
67
+ return await Promise.all(
68
+ steps.map(async (substep) => {
69
+ const annotated = annotateSubstep(substep, {
70
+ callDepth,
71
+ callPath: [...stack],
72
+ });
73
+
74
+ if (annotated.type !== 'play_call') {
75
+ return annotated;
76
+ }
77
+
78
+ if (stack.includes(annotated.playId)) {
79
+ return {
80
+ ...annotated,
81
+ cycleDetected: true,
82
+ resolutionError: `Static cycle detected: ${[...stack, annotated.playId].join(' -> ')}`,
83
+ pipeline: null,
84
+ } satisfies PlayStaticSubstep;
85
+ }
86
+
87
+ if (stack.length >= maxDepth) {
88
+ return {
89
+ ...annotated,
90
+ resolutionError: `Static nesting limit reached at ${annotated.playId}`,
91
+ pipeline: null,
92
+ } satisfies PlayStaticSubstep;
93
+ }
94
+
95
+ const childShape = await resolveReferencedPlay(annotated.playId);
96
+ const childPipeline = materializePipeline(childShape);
97
+ if (!childPipeline) {
98
+ return {
99
+ ...annotated,
100
+ resolutionError: `Unable to statically resolve play "${annotated.playId}"`,
101
+ pipeline: null,
102
+ } satisfies PlayStaticSubstep;
103
+ }
104
+
105
+ const resolvedChildPipeline = {
106
+ ...childPipeline,
107
+ stages: await resolvePipelineSubsteps({
108
+ steps: childPipeline.stages ?? [],
109
+ resolveReferencedPlay,
110
+ stack: [...stack, annotated.playId],
111
+ maxDepth,
112
+ }),
113
+ substeps: await resolvePipelineSubsteps({
114
+ steps: childPipeline.substeps,
115
+ resolveReferencedPlay,
116
+ stack: [...stack, annotated.playId],
117
+ maxDepth,
118
+ }),
119
+ };
120
+ const childContract = compileSheetContract(resolvedChildPipeline);
121
+
122
+ return {
123
+ ...annotated,
124
+ pipeline: {
125
+ ...resolvedChildPipeline,
126
+ sheetContract: childContract.contract,
127
+ sheetContractErrors: childContract.errors,
128
+ },
129
+ } satisfies PlayStaticSubstep;
130
+ }),
131
+ );
132
+ }
133
+
134
+ export async function resolveStaticPipelineTree(
135
+ input: ResolveStaticPipelineInput,
136
+ ): Promise<PlayStaticPipeline | null> {
137
+ const maxDepth = input.maxDepth ?? 6;
138
+ const rootPipeline = materializePipeline(input.root);
139
+ if (!rootPipeline) {
140
+ return null;
141
+ }
142
+
143
+ const resolvedPipeline = {
144
+ ...rootPipeline,
145
+ stages: await resolvePipelineSubsteps({
146
+ steps: rootPipeline.stages ?? [],
147
+ resolveReferencedPlay: input.resolveReferencedPlay,
148
+ stack: [input.rootPlayId],
149
+ maxDepth,
150
+ }),
151
+ substeps: await resolvePipelineSubsteps({
152
+ steps: rootPipeline.substeps,
153
+ resolveReferencedPlay: input.resolveReferencedPlay,
154
+ stack: [input.rootPlayId],
155
+ maxDepth,
156
+ }),
157
+ };
158
+ const contract = compileSheetContract(resolvedPipeline);
159
+ return {
160
+ ...resolvedPipeline,
161
+ sheetContract: contract.contract,
162
+ sheetContractErrors: contract.errors,
163
+ };
164
+ }