deepline 0.1.182 → 0.1.184
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/apps/play-runner-workers/src/child-play-await.ts +203 -6
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +639 -109
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1752 -1899
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
- package/dist/bundling-sources/sdk/src/client.ts +41 -0
- package/dist/bundling-sources/sdk/src/http.ts +23 -8
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +133 -10
- package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
- package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1612 -287
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
- package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2038 -262
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +28 -11
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +38 -6
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
- package/dist/cli/index.js +76 -17
- package/dist/cli/index.mjs +76 -17
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -9
- package/dist/index.mjs +63 -9
- package/dist/plays/bundle-play-file.mjs +20 -6
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
export type WorkReceiptStateStatus =
|
|
2
|
+
| 'pending'
|
|
3
|
+
| 'running'
|
|
4
|
+
| 'completed'
|
|
5
|
+
| 'failed'
|
|
6
|
+
| 'skipped';
|
|
7
|
+
|
|
8
|
+
export type WorkReceiptStateInput = {
|
|
9
|
+
status: WorkReceiptStateStatus;
|
|
10
|
+
runId?: string | null;
|
|
11
|
+
failureKind?: 'terminal' | 'repairable' | null;
|
|
12
|
+
leaseId?: string | null;
|
|
13
|
+
leaseOwnerRunId?: string | null;
|
|
14
|
+
leaseOwnerAttempt?: number | null;
|
|
15
|
+
leaseExpiresAt?: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WorkReceiptLeaseState =
|
|
19
|
+
| { kind: 'none' }
|
|
20
|
+
| {
|
|
21
|
+
kind: 'active';
|
|
22
|
+
leaseId: string;
|
|
23
|
+
ownerRunId: string;
|
|
24
|
+
ownerAttempt: number;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
expiresAtMs: number;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
kind: 'expired';
|
|
30
|
+
leaseId: string | null;
|
|
31
|
+
ownerRunId: string;
|
|
32
|
+
ownerAttempt: number;
|
|
33
|
+
expiresAt: string;
|
|
34
|
+
expiresAtMs: number;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
kind: 'invalid';
|
|
38
|
+
reason:
|
|
39
|
+
| 'missing_owner'
|
|
40
|
+
| 'missing_expiry'
|
|
41
|
+
| 'missing_lease_id'
|
|
42
|
+
| 'invalid_expiry';
|
|
43
|
+
leaseId: string | null;
|
|
44
|
+
ownerRunId: string | null;
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WorkReceiptInvalidLeaseReason = Extract<
|
|
49
|
+
WorkReceiptLeaseState,
|
|
50
|
+
{ kind: 'invalid' }
|
|
51
|
+
>['reason'];
|
|
52
|
+
|
|
53
|
+
export type WorkReceiptState =
|
|
54
|
+
| {
|
|
55
|
+
kind: 'pending';
|
|
56
|
+
terminal: false;
|
|
57
|
+
reusable: false;
|
|
58
|
+
lease: WorkReceiptLeaseState;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
kind: 'running';
|
|
62
|
+
terminal: false;
|
|
63
|
+
reusable: false;
|
|
64
|
+
lease: WorkReceiptLeaseState;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: 'completed';
|
|
68
|
+
terminal: true;
|
|
69
|
+
reusable: true;
|
|
70
|
+
lease: WorkReceiptLeaseState;
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
kind: 'skipped';
|
|
74
|
+
terminal: true;
|
|
75
|
+
reusable: true;
|
|
76
|
+
lease: WorkReceiptLeaseState;
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: 'failed';
|
|
80
|
+
terminal: true;
|
|
81
|
+
reusable: false;
|
|
82
|
+
lease: WorkReceiptLeaseState;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type WorkReceiptClaimDecision =
|
|
86
|
+
| { kind: 'claim' }
|
|
87
|
+
| { kind: 'reuse_terminal' }
|
|
88
|
+
| {
|
|
89
|
+
kind: 'blocked_active_lease';
|
|
90
|
+
status: WorkReceiptStateStatus;
|
|
91
|
+
ownerRunId: string | null;
|
|
92
|
+
leaseId: string | null;
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
kind: 'blocked_running';
|
|
96
|
+
ownerRunId: string | null;
|
|
97
|
+
leaseId: string | null;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
kind: 'blocked_invalid_lease';
|
|
101
|
+
reason: WorkReceiptInvalidLeaseReason;
|
|
102
|
+
status: WorkReceiptStateStatus;
|
|
103
|
+
}
|
|
104
|
+
| { kind: 'blocked_failed' };
|
|
105
|
+
|
|
106
|
+
export type WorkReceiptOwnerOperationDecision =
|
|
107
|
+
| { kind: 'allow' }
|
|
108
|
+
| {
|
|
109
|
+
kind: 'reject';
|
|
110
|
+
reason:
|
|
111
|
+
| 'missing_receipt'
|
|
112
|
+
| 'not_running'
|
|
113
|
+
| 'owner_mismatch'
|
|
114
|
+
| 'attempt_mismatch'
|
|
115
|
+
| 'lease_mismatch'
|
|
116
|
+
| 'missing_request_lease';
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type WorkReceiptOwnerOperationInput = {
|
|
120
|
+
receipt: WorkReceiptStateInput | null;
|
|
121
|
+
ownerRunId?: string | null;
|
|
122
|
+
ownerRunAttempt?: number | null;
|
|
123
|
+
leaseId?: string | null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function normalize(value: string | null | undefined): string | null {
|
|
127
|
+
const trimmed = value?.trim();
|
|
128
|
+
return trimmed ? trimmed : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeAttempt(value: number | null | undefined): number {
|
|
132
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
133
|
+
? Math.max(0, Math.floor(value))
|
|
134
|
+
: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function classifyWorkReceiptLeaseState(
|
|
138
|
+
receipt: WorkReceiptStateInput,
|
|
139
|
+
nowMs: number,
|
|
140
|
+
): WorkReceiptLeaseState {
|
|
141
|
+
const ownerRunId =
|
|
142
|
+
normalize(receipt.leaseOwnerRunId) ?? normalize(receipt.runId);
|
|
143
|
+
const leaseId = normalize(receipt.leaseId);
|
|
144
|
+
const expiresAt = normalize(receipt.leaseExpiresAt);
|
|
145
|
+
if (!leaseId && !expiresAt) {
|
|
146
|
+
return { kind: 'none' };
|
|
147
|
+
}
|
|
148
|
+
if (!ownerRunId) {
|
|
149
|
+
return {
|
|
150
|
+
kind: 'invalid',
|
|
151
|
+
reason: 'missing_owner',
|
|
152
|
+
leaseId,
|
|
153
|
+
ownerRunId,
|
|
154
|
+
expiresAt,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!expiresAt) {
|
|
158
|
+
return {
|
|
159
|
+
kind: 'invalid',
|
|
160
|
+
reason: 'missing_expiry',
|
|
161
|
+
leaseId,
|
|
162
|
+
ownerRunId,
|
|
163
|
+
expiresAt,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
167
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
168
|
+
return {
|
|
169
|
+
kind: 'invalid',
|
|
170
|
+
reason: 'invalid_expiry',
|
|
171
|
+
leaseId,
|
|
172
|
+
ownerRunId,
|
|
173
|
+
expiresAt,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (expiresAtMs <= nowMs) {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'expired',
|
|
179
|
+
leaseId,
|
|
180
|
+
ownerRunId,
|
|
181
|
+
ownerAttempt: normalizeAttempt(receipt.leaseOwnerAttempt),
|
|
182
|
+
expiresAt,
|
|
183
|
+
expiresAtMs,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!leaseId) {
|
|
187
|
+
return {
|
|
188
|
+
kind: 'invalid',
|
|
189
|
+
reason: 'missing_lease_id',
|
|
190
|
+
leaseId,
|
|
191
|
+
ownerRunId,
|
|
192
|
+
expiresAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
kind: 'active',
|
|
197
|
+
leaseId,
|
|
198
|
+
ownerRunId,
|
|
199
|
+
ownerAttempt: normalizeAttempt(receipt.leaseOwnerAttempt),
|
|
200
|
+
expiresAt,
|
|
201
|
+
expiresAtMs,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function classifyWorkReceiptState(
|
|
206
|
+
receipt: WorkReceiptStateInput,
|
|
207
|
+
nowMs: number,
|
|
208
|
+
): WorkReceiptState {
|
|
209
|
+
const lease = classifyWorkReceiptLeaseState(receipt, nowMs);
|
|
210
|
+
switch (receipt.status) {
|
|
211
|
+
case 'pending':
|
|
212
|
+
return { kind: 'pending', terminal: false, reusable: false, lease };
|
|
213
|
+
case 'running':
|
|
214
|
+
return { kind: 'running', terminal: false, reusable: false, lease };
|
|
215
|
+
case 'completed':
|
|
216
|
+
return { kind: 'completed', terminal: true, reusable: true, lease };
|
|
217
|
+
case 'skipped':
|
|
218
|
+
return { kind: 'skipped', terminal: true, reusable: true, lease };
|
|
219
|
+
case 'failed':
|
|
220
|
+
return { kind: 'failed', terminal: true, reusable: false, lease };
|
|
221
|
+
default:
|
|
222
|
+
return assertNever(receipt.status);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function decideWorkReceiptClaim(input: {
|
|
227
|
+
receipt: WorkReceiptStateInput | null;
|
|
228
|
+
claimantRunId?: string | null;
|
|
229
|
+
claimantRunAttempt?: number | null;
|
|
230
|
+
reclaimRunning?: boolean;
|
|
231
|
+
forceRefresh?: boolean;
|
|
232
|
+
forceFailedRefresh?: boolean;
|
|
233
|
+
nowMs: number;
|
|
234
|
+
}): WorkReceiptClaimDecision {
|
|
235
|
+
if (!input.receipt) return { kind: 'claim' };
|
|
236
|
+
const state = classifyWorkReceiptState(input.receipt, input.nowMs);
|
|
237
|
+
if (state.reusable && input.forceRefresh !== true) {
|
|
238
|
+
return { kind: 'reuse_terminal' };
|
|
239
|
+
}
|
|
240
|
+
if (state.kind === 'failed' && input.forceRefresh !== true) {
|
|
241
|
+
if (input.forceFailedRefresh === true) {
|
|
242
|
+
return { kind: 'claim' };
|
|
243
|
+
}
|
|
244
|
+
const claimantRunId = normalize(input.claimantRunId);
|
|
245
|
+
const ownerRunId = normalize(input.receipt.runId);
|
|
246
|
+
const claimantRunAttempt = normalizeAttempt(input.claimantRunAttempt);
|
|
247
|
+
const ownerAttempt = normalizeAttempt(input.receipt.leaseOwnerAttempt);
|
|
248
|
+
if (
|
|
249
|
+
input.receipt.failureKind === 'repairable' &&
|
|
250
|
+
claimantRunId !== null &&
|
|
251
|
+
ownerRunId !== null &&
|
|
252
|
+
(claimantRunId !== ownerRunId || ownerAttempt < claimantRunAttempt)
|
|
253
|
+
) {
|
|
254
|
+
return { kind: 'claim' };
|
|
255
|
+
}
|
|
256
|
+
return { kind: 'blocked_failed' };
|
|
257
|
+
}
|
|
258
|
+
if (state.kind === 'pending' || state.kind === 'running') {
|
|
259
|
+
if (state.lease.kind === 'active') {
|
|
260
|
+
if (
|
|
261
|
+
(state.kind !== 'running' ||
|
|
262
|
+
input.reclaimRunning === true ||
|
|
263
|
+
input.forceRefresh === true) &&
|
|
264
|
+
state.lease.ownerRunId !== null &&
|
|
265
|
+
state.lease.ownerRunId === normalize(input.claimantRunId) &&
|
|
266
|
+
state.lease.ownerAttempt < normalizeAttempt(input.claimantRunAttempt)
|
|
267
|
+
) {
|
|
268
|
+
return { kind: 'claim' };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
kind: 'blocked_active_lease',
|
|
272
|
+
status: state.kind,
|
|
273
|
+
ownerRunId: state.lease.ownerRunId,
|
|
274
|
+
leaseId: state.lease.leaseId,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (state.lease.kind === 'invalid') {
|
|
278
|
+
return {
|
|
279
|
+
kind: 'blocked_invalid_lease',
|
|
280
|
+
reason: state.lease.reason,
|
|
281
|
+
status: state.kind,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (input.forceRefresh === true && input.reclaimRunning === true) {
|
|
285
|
+
return { kind: 'claim' };
|
|
286
|
+
}
|
|
287
|
+
if (state.kind === 'pending') {
|
|
288
|
+
return { kind: 'claim' };
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
input.reclaimRunning === true &&
|
|
292
|
+
(state.lease.kind === 'expired' || state.lease.kind === 'none')
|
|
293
|
+
) {
|
|
294
|
+
return { kind: 'claim' };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
kind: 'blocked_running',
|
|
298
|
+
ownerRunId:
|
|
299
|
+
state.lease.kind === 'expired'
|
|
300
|
+
? state.lease.ownerRunId
|
|
301
|
+
: normalize(input.receipt.runId),
|
|
302
|
+
leaseId:
|
|
303
|
+
state.lease.kind === 'expired'
|
|
304
|
+
? state.lease.leaseId
|
|
305
|
+
: normalize(input.receipt.leaseId),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
input.forceRefresh === true &&
|
|
310
|
+
(state.kind === 'completed' || state.kind === 'skipped')
|
|
311
|
+
) {
|
|
312
|
+
if (state.lease.kind === 'active') {
|
|
313
|
+
if (
|
|
314
|
+
state.lease.ownerRunId !== null &&
|
|
315
|
+
state.lease.ownerRunId === normalize(input.claimantRunId) &&
|
|
316
|
+
state.lease.ownerAttempt < normalizeAttempt(input.claimantRunAttempt)
|
|
317
|
+
) {
|
|
318
|
+
return { kind: 'claim' };
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
kind: 'blocked_active_lease',
|
|
322
|
+
status: state.kind,
|
|
323
|
+
ownerRunId: state.lease.ownerRunId,
|
|
324
|
+
leaseId: state.lease.leaseId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (state.lease.kind === 'invalid') {
|
|
328
|
+
return {
|
|
329
|
+
kind: 'blocked_invalid_lease',
|
|
330
|
+
reason: state.lease.reason,
|
|
331
|
+
status: state.kind,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (input.forceRefresh === true) {
|
|
336
|
+
return { kind: 'claim' };
|
|
337
|
+
}
|
|
338
|
+
return { kind: 'claim' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function ownerRunIdForReceipt(receipt: WorkReceiptStateInput): string | null {
|
|
342
|
+
return normalize(receipt.leaseOwnerRunId) ?? normalize(receipt.runId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function baseRunningOwnerDecision(
|
|
346
|
+
input: WorkReceiptOwnerOperationInput,
|
|
347
|
+
): WorkReceiptOwnerOperationDecision {
|
|
348
|
+
if (!input.receipt) {
|
|
349
|
+
return { kind: 'reject', reason: 'missing_receipt' };
|
|
350
|
+
}
|
|
351
|
+
if (input.receipt.status !== 'running') {
|
|
352
|
+
return { kind: 'reject', reason: 'not_running' };
|
|
353
|
+
}
|
|
354
|
+
const ownerRunId = normalize(input.ownerRunId);
|
|
355
|
+
if (!ownerRunId || ownerRunIdForReceipt(input.receipt) !== ownerRunId) {
|
|
356
|
+
return { kind: 'reject', reason: 'owner_mismatch' };
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
normalizeAttempt(input.receipt.leaseOwnerAttempt) !==
|
|
360
|
+
normalizeAttempt(input.ownerRunAttempt)
|
|
361
|
+
) {
|
|
362
|
+
return { kind: 'reject', reason: 'attempt_mismatch' };
|
|
363
|
+
}
|
|
364
|
+
return { kind: 'allow' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Law for complete/fail/skip. A terminal write may only be made by the
|
|
369
|
+
* running receipt owner for the same attempt. When the receipt has a lease id,
|
|
370
|
+
* the writer must also present that exact lease id; legacy unleased running
|
|
371
|
+
* receipts remain writable by their run+attempt owner during the migration.
|
|
372
|
+
*/
|
|
373
|
+
export function decideWorkReceiptTerminalWrite(
|
|
374
|
+
input: WorkReceiptOwnerOperationInput,
|
|
375
|
+
): WorkReceiptOwnerOperationDecision {
|
|
376
|
+
const ownerDecision = baseRunningOwnerDecision(input);
|
|
377
|
+
if (ownerDecision.kind === 'reject') return ownerDecision;
|
|
378
|
+
|
|
379
|
+
const receiptLeaseId = normalize(input.receipt?.leaseId);
|
|
380
|
+
if (!receiptLeaseId) return { kind: 'allow' };
|
|
381
|
+
if (receiptLeaseId !== normalize(input.leaseId)) {
|
|
382
|
+
return { kind: 'reject', reason: 'lease_mismatch' };
|
|
383
|
+
}
|
|
384
|
+
return { kind: 'allow' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Law for a single suspended receipt release. It uses the same ownership fence
|
|
389
|
+
* as terminal writes: only the running owner can release, and a leased receipt
|
|
390
|
+
* requires the exact lease id.
|
|
391
|
+
*/
|
|
392
|
+
export function decideWorkReceiptExactRelease(
|
|
393
|
+
input: WorkReceiptOwnerOperationInput,
|
|
394
|
+
): WorkReceiptOwnerOperationDecision {
|
|
395
|
+
return decideWorkReceiptTerminalWrite(input);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Law for run-fatal teardown. The owner may release all of its still-running
|
|
400
|
+
* receipts for the same attempt without threading every per-receipt lease id.
|
|
401
|
+
* It must never match another run or another attempt.
|
|
402
|
+
*/
|
|
403
|
+
export function decideWorkReceiptOwnerRelease(
|
|
404
|
+
input: WorkReceiptOwnerOperationInput,
|
|
405
|
+
): WorkReceiptOwnerOperationDecision {
|
|
406
|
+
return baseRunningOwnerDecision(input);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Law for heartbeat. Heartbeats are stricter than terminal writes: they renew a
|
|
411
|
+
* specific live lease, so both receipt and request must carry the same lease id.
|
|
412
|
+
*/
|
|
413
|
+
export function decideWorkReceiptHeartbeat(
|
|
414
|
+
input: WorkReceiptOwnerOperationInput,
|
|
415
|
+
): WorkReceiptOwnerOperationDecision {
|
|
416
|
+
const ownerDecision = baseRunningOwnerDecision(input);
|
|
417
|
+
if (ownerDecision.kind === 'reject') return ownerDecision;
|
|
418
|
+
|
|
419
|
+
const requestLeaseId = normalize(input.leaseId);
|
|
420
|
+
if (!requestLeaseId) {
|
|
421
|
+
return { kind: 'reject', reason: 'missing_request_lease' };
|
|
422
|
+
}
|
|
423
|
+
if (normalize(input.receipt?.leaseId) !== requestLeaseId) {
|
|
424
|
+
return { kind: 'reject', reason: 'lease_mismatch' };
|
|
425
|
+
}
|
|
426
|
+
return { kind: 'allow' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function isReusableWorkReceiptState(
|
|
430
|
+
receipt: WorkReceiptStateInput,
|
|
431
|
+
): boolean {
|
|
432
|
+
return receipt.status === 'completed' || receipt.status === 'skipped';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function assertNever(value: never): never {
|
|
436
|
+
throw new Error(`Unhandled work receipt state: ${String(value)}`);
|
|
437
|
+
}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
import { isReusableWorkReceiptState } from './work-receipt-state-machine';
|
|
2
|
+
|
|
3
|
+
export type WorkReceiptFailureKind = 'terminal' | 'repairable';
|
|
4
|
+
|
|
5
|
+
export const WORK_RECEIPT_FAILURE_KIND_CODE: Record<
|
|
6
|
+
WorkReceiptFailureKind,
|
|
7
|
+
number
|
|
8
|
+
> = {
|
|
9
|
+
terminal: 0,
|
|
10
|
+
repairable: 1,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function workReceiptFailureKindFromCode(
|
|
14
|
+
value: unknown,
|
|
15
|
+
): WorkReceiptFailureKind {
|
|
16
|
+
return Number(value) === WORK_RECEIPT_FAILURE_KIND_CODE.repairable
|
|
17
|
+
? 'repairable'
|
|
18
|
+
: 'terminal';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function workReceiptFailureKindCode(
|
|
22
|
+
value: WorkReceiptFailureKind | null | undefined,
|
|
23
|
+
): number {
|
|
24
|
+
return WORK_RECEIPT_FAILURE_KIND_CODE[value ?? 'terminal'];
|
|
25
|
+
}
|
|
26
|
+
|
|
1
27
|
export type WorkReceiptStatus =
|
|
2
28
|
| 'pending'
|
|
3
29
|
| 'running'
|
|
@@ -10,7 +36,12 @@ export type WorkReceipt = {
|
|
|
10
36
|
status: WorkReceiptStatus;
|
|
11
37
|
output?: unknown;
|
|
12
38
|
error?: string | null;
|
|
39
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
13
40
|
runId?: string | null;
|
|
41
|
+
leaseId?: string | null;
|
|
42
|
+
leaseOwnerRunId?: string | null;
|
|
43
|
+
leaseOwnerAttempt?: number | null;
|
|
44
|
+
leaseExpiresAt?: string | null;
|
|
14
45
|
updatedAt?: string | null;
|
|
15
46
|
};
|
|
16
47
|
|
|
@@ -18,26 +49,57 @@ export type WorkReceiptCommand = {
|
|
|
18
49
|
playName: string;
|
|
19
50
|
runId: string;
|
|
20
51
|
key: string;
|
|
52
|
+
leaseId?: string | null;
|
|
53
|
+
runAttempt?: number | null;
|
|
54
|
+
leaseTtlMs?: number | null;
|
|
55
|
+
leaseAware?: boolean;
|
|
21
56
|
reclaimRunning?: boolean;
|
|
22
57
|
forceRefresh?: boolean;
|
|
58
|
+
forceFailedRefresh?: boolean;
|
|
23
59
|
};
|
|
24
60
|
|
|
25
61
|
export type WorkReceiptBatchClaimCommand = {
|
|
26
62
|
playName: string;
|
|
27
63
|
runId: string;
|
|
28
64
|
keys: string[];
|
|
65
|
+
leaseAware?: boolean;
|
|
29
66
|
reclaimRunning?: boolean;
|
|
30
67
|
forceRefresh?: boolean;
|
|
68
|
+
forceFailedRefresh?: boolean;
|
|
69
|
+
runAttempt?: number | null;
|
|
70
|
+
leaseTtlMs?: number | null;
|
|
31
71
|
};
|
|
32
72
|
|
|
33
73
|
export type WorkReceiptBatchCompleteCommand = {
|
|
34
74
|
playName: string;
|
|
35
|
-
receipts: Array<{
|
|
75
|
+
receipts: Array<{
|
|
76
|
+
runId: string;
|
|
77
|
+
key: string;
|
|
78
|
+
output: unknown;
|
|
79
|
+
leaseId?: string | null;
|
|
80
|
+
runAttempt?: number | null;
|
|
81
|
+
}>;
|
|
36
82
|
};
|
|
37
83
|
|
|
38
84
|
export type WorkReceiptBatchFailCommand = {
|
|
39
85
|
playName: string;
|
|
40
|
-
receipts: Array<{
|
|
86
|
+
receipts: Array<{
|
|
87
|
+
runId: string;
|
|
88
|
+
key: string;
|
|
89
|
+
error: string;
|
|
90
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
91
|
+
leaseId?: string | null;
|
|
92
|
+
runAttempt?: number | null;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type WorkReceiptBatchHeartbeatCommand = {
|
|
97
|
+
playName: string;
|
|
98
|
+
runId: string;
|
|
99
|
+
runAttempt?: number | null;
|
|
100
|
+
leaseId: string;
|
|
101
|
+
keys: string[];
|
|
102
|
+
leaseTtlMs?: number | null;
|
|
41
103
|
};
|
|
42
104
|
|
|
43
105
|
export type WorkReceiptGetCommand = {
|
|
@@ -58,6 +120,10 @@ export type WorkReceiptClaim =
|
|
|
58
120
|
disposition: 'running';
|
|
59
121
|
receipt: WorkReceipt;
|
|
60
122
|
}
|
|
123
|
+
| {
|
|
124
|
+
disposition: 'blocked_active_lease';
|
|
125
|
+
receipt: WorkReceipt;
|
|
126
|
+
}
|
|
61
127
|
| {
|
|
62
128
|
disposition: 'failed';
|
|
63
129
|
receipt: WorkReceipt;
|
|
@@ -74,11 +140,22 @@ export type WorkReceiptStore = {
|
|
|
74
140
|
): Promise<WorkReceipt | null>;
|
|
75
141
|
completeReceipts?(
|
|
76
142
|
input: WorkReceiptBatchCompleteCommand,
|
|
77
|
-
): Promise<WorkReceipt
|
|
143
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
78
144
|
failReceipt(
|
|
79
|
-
input: WorkReceiptCommand & {
|
|
145
|
+
input: WorkReceiptCommand & {
|
|
146
|
+
error: string;
|
|
147
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
148
|
+
},
|
|
149
|
+
): Promise<WorkReceipt | null>;
|
|
150
|
+
failReceipts?(
|
|
151
|
+
input: WorkReceiptBatchFailCommand,
|
|
152
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
153
|
+
releaseReceipt?(
|
|
154
|
+
input: WorkReceiptCommand,
|
|
80
155
|
): Promise<WorkReceipt | null>;
|
|
81
|
-
|
|
156
|
+
heartbeatReceipts?(
|
|
157
|
+
input: WorkReceiptBatchHeartbeatCommand,
|
|
158
|
+
): Promise<Array<WorkReceipt | null>>;
|
|
82
159
|
};
|
|
83
160
|
|
|
84
161
|
const SCOPED_WORK_RECEIPT_KEY_VERSION = 'ctxv2';
|
|
@@ -122,5 +199,5 @@ export function scopedWorkReceiptKeyPrefix(input: {
|
|
|
122
199
|
}
|
|
123
200
|
|
|
124
201
|
export function isReusableWorkReceipt(receipt: WorkReceipt): boolean {
|
|
125
|
-
return receipt
|
|
202
|
+
return isReusableWorkReceiptState(receipt);
|
|
126
203
|
}
|
|
@@ -475,7 +475,9 @@ export function extractDefinedPlayDescriptionForExport(
|
|
|
475
475
|
sourceCode: string,
|
|
476
476
|
exportName: string,
|
|
477
477
|
): string | null {
|
|
478
|
-
return
|
|
478
|
+
return (
|
|
479
|
+
extractDefinedPlayMetadata(sourceCode, exportName)?.description ?? null
|
|
480
|
+
);
|
|
479
481
|
}
|
|
480
482
|
|
|
481
483
|
type AstNode = {
|
|
@@ -548,7 +550,9 @@ function astArray(value: unknown): AstNode[] {
|
|
|
548
550
|
return Array.isArray(value) ? value.filter(isAstNode) : [];
|
|
549
551
|
}
|
|
550
552
|
|
|
551
|
-
function memberExpressionPath(
|
|
553
|
+
function memberExpressionPath(
|
|
554
|
+
node: AstNode | null | undefined,
|
|
555
|
+
): string[] | null {
|
|
552
556
|
const expression = unwrapStaticExpression(node);
|
|
553
557
|
if (!expression) return null;
|
|
554
558
|
if (expression.type === 'Identifier' && typeof expression.name === 'string') {
|
|
@@ -657,7 +661,9 @@ function buildPlayMetadataContext(ast: AstNode): PlayMetadataExtractionContext {
|
|
|
657
661
|
isAstNode(statement.declaration) &&
|
|
658
662
|
statement.declaration.type === 'VariableDeclaration'
|
|
659
663
|
) {
|
|
660
|
-
for (const declaration of astArray(
|
|
664
|
+
for (const declaration of astArray(
|
|
665
|
+
statement.declaration.declarations,
|
|
666
|
+
)) {
|
|
661
667
|
const name = getIdentifierName(declaration.id);
|
|
662
668
|
if (!name) continue;
|
|
663
669
|
const initializer = isAstNode(declaration.init)
|
|
@@ -708,7 +714,9 @@ function buildPlayMetadataContext(ast: AstNode): PlayMetadataExtractionContext {
|
|
|
708
714
|
return { declarations, namedExports, commonJsExports, defaultExport };
|
|
709
715
|
}
|
|
710
716
|
|
|
711
|
-
function unwrapStaticExpression(
|
|
717
|
+
function unwrapStaticExpression(
|
|
718
|
+
node: AstNode | null | undefined,
|
|
719
|
+
): AstNode | null {
|
|
712
720
|
let current = node ?? null;
|
|
713
721
|
while (
|
|
714
722
|
current &&
|
|
@@ -837,9 +845,7 @@ function stringPropertyFromObjectExpression(
|
|
|
837
845
|
|
|
838
846
|
if (property.type !== 'Property') continue;
|
|
839
847
|
if (propertyNameFromKey(property) !== propertyName) continue;
|
|
840
|
-
const propertyValue = property.shorthand
|
|
841
|
-
? property.key
|
|
842
|
-
: property.value;
|
|
848
|
+
const propertyValue = property.shorthand ? property.key : property.value;
|
|
843
849
|
value = staticStringFromExpression(
|
|
844
850
|
isAstNode(propertyValue) ? propertyValue : null,
|
|
845
851
|
context,
|
|
@@ -883,11 +889,16 @@ function playMetadataFromDefinePlayCall(
|
|
|
883
889
|
|
|
884
890
|
const args = astArray(expression.arguments);
|
|
885
891
|
const firstArg = args[0] ?? null;
|
|
886
|
-
const objectName = stringPropertyFromObjectExpression(
|
|
892
|
+
const objectName = stringPropertyFromObjectExpression(
|
|
893
|
+
firstArg,
|
|
894
|
+
'id',
|
|
895
|
+
context,
|
|
896
|
+
);
|
|
887
897
|
const name = objectName ?? staticStringFromExpression(firstArg, context);
|
|
888
898
|
|
|
889
899
|
let description =
|
|
890
|
-
stringPropertyFromObjectExpression(firstArg, 'description', context) ??
|
|
900
|
+
stringPropertyFromObjectExpression(firstArg, 'description', context) ??
|
|
901
|
+
null;
|
|
891
902
|
for (let index = args.length - 1; !description && index >= 2; index -= 1) {
|
|
892
903
|
description = stringPropertyFromObjectExpression(
|
|
893
904
|
args[index],
|
|
@@ -1608,7 +1619,10 @@ async function analyzeSourceGraph(
|
|
|
1608
1619
|
}
|
|
1609
1620
|
visited.add(absolutePath);
|
|
1610
1621
|
|
|
1611
|
-
const sourceCode = await readFile(
|
|
1622
|
+
const sourceCode = await readFile(
|
|
1623
|
+
/* turbopackIgnore: true */ absolutePath,
|
|
1624
|
+
'utf-8',
|
|
1625
|
+
);
|
|
1612
1626
|
localFiles.set(absolutePath, sourceCode);
|
|
1613
1627
|
|
|
1614
1628
|
if (extname(absolutePath).toLowerCase() === '.json') {
|
|
@@ -1645,7 +1659,10 @@ async function analyzeSourceGraph(
|
|
|
1645
1659
|
column,
|
|
1646
1660
|
});
|
|
1647
1661
|
if (resolved !== absoluteEntryFile && isPlaySourceFile(resolved)) {
|
|
1648
|
-
const importedSource = await readFile(
|
|
1662
|
+
const importedSource = await readFile(
|
|
1663
|
+
/* turbopackIgnore: true */ resolved,
|
|
1664
|
+
'utf-8',
|
|
1665
|
+
);
|
|
1649
1666
|
const importedPlayName = extractDefinedPlayName(importedSource);
|
|
1650
1667
|
if (!importedPlayName) {
|
|
1651
1668
|
throw new Error(
|