deepline 0.1.212 → 0.1.214
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/entry.ts +167 -329
- package/dist/bundling-sources/sdk/src/client.ts +2 -2
- package/dist/bundling-sources/sdk/src/index.ts +2 -0
- package/dist/bundling-sources/sdk/src/play.ts +8 -1
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -1
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +3 -3
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +49 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
- package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +209 -123
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +30 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +160 -2
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +54 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +477 -85
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
- package/dist/cli/index.js +50 -436
- package/dist/cli/index.mjs +50 -436
- package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
- package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
- package/dist/index.d.mts +11 -8
- package/dist/index.d.ts +11 -8
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +1 -1
- package/package.json +1 -1
|
@@ -1563,8 +1563,8 @@ export class DeeplineClient {
|
|
|
1563
1563
|
? { waitForCompletionMs: request.waitForCompletionMs }
|
|
1564
1564
|
: {}),
|
|
1565
1565
|
// Profile selection is the API's job, not the CLI's. The server
|
|
1566
|
-
// defaults to
|
|
1567
|
-
//
|
|
1566
|
+
// defaults to absurd; callers that need workers_edge pass
|
|
1567
|
+
// `request.profile` explicitly.
|
|
1568
1568
|
...(request.profile ? { profile: request.profile } : {}),
|
|
1569
1569
|
...(integrationMode ? { integrationMode } : {}),
|
|
1570
1570
|
...(testPolicyOverrides ? { testPolicyOverrides } : {}),
|
|
@@ -114,6 +114,13 @@ import type {
|
|
|
114
114
|
} from './types.js';
|
|
115
115
|
import type { ToolExecution } from './client.js';
|
|
116
116
|
|
|
117
|
+
export type PlayCallExecution = 'inline' | 'child-workflow';
|
|
118
|
+
|
|
119
|
+
export interface PlayCallOptions {
|
|
120
|
+
description?: string;
|
|
121
|
+
execution?: PlayCallExecution;
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
/**
|
|
118
125
|
* Optional trigger bindings for a play.
|
|
119
126
|
*
|
|
@@ -891,7 +898,7 @@ export interface DeeplinePlayRuntimeContext {
|
|
|
891
898
|
key: string,
|
|
892
899
|
playRef: string | PlayReferenceLike,
|
|
893
900
|
input: Record<string, unknown>,
|
|
894
|
-
options
|
|
901
|
+
options?: PlayCallOptions,
|
|
895
902
|
): Promise<TOutput>;
|
|
896
903
|
|
|
897
904
|
/**
|
|
@@ -274,6 +274,7 @@ export async function harnessStartSheetDataset(input: {
|
|
|
274
274
|
attemptExpiresAt?: string | null;
|
|
275
275
|
attemptSeq?: number | null;
|
|
276
276
|
attemptLeaseTtlMs?: number | null;
|
|
277
|
+
writeVersion?: number | null;
|
|
277
278
|
inputOffset?: number;
|
|
278
279
|
force?: boolean;
|
|
279
280
|
userEmail?: string | null;
|
|
@@ -288,6 +289,7 @@ export async function harnessStartSheetDataset(input: {
|
|
|
288
289
|
attemptOwnerRunId?: string;
|
|
289
290
|
attemptExpiresAt?: string;
|
|
290
291
|
attemptSeq?: number;
|
|
292
|
+
writeVersion?: number;
|
|
291
293
|
timings?: Array<Record<string, unknown>>;
|
|
292
294
|
}> {
|
|
293
295
|
return requireBinding().startSheetDataset(input);
|
|
@@ -312,8 +314,16 @@ export async function harnessPersistCompletedSheetRows(input: {
|
|
|
312
314
|
attemptOwnerRunId?: string | null;
|
|
313
315
|
attemptExpiresAt?: string | null;
|
|
314
316
|
attemptSeq?: number | null;
|
|
317
|
+
writeVersion?: number | null;
|
|
315
318
|
forceFailedRows?: boolean;
|
|
316
319
|
userEmail?: string | null;
|
|
317
|
-
}): Promise<{
|
|
320
|
+
}): Promise<{
|
|
321
|
+
ok: true;
|
|
322
|
+
rowsWritten: number;
|
|
323
|
+
tableNamespace: string;
|
|
324
|
+
fencedKeys?: string[];
|
|
325
|
+
staleDropped?: number;
|
|
326
|
+
staleDroppedKeys?: string[];
|
|
327
|
+
}> {
|
|
318
328
|
return requireBinding().persistCompletedMapRows(input);
|
|
319
329
|
}
|
|
@@ -106,10 +106,10 @@ export const SDK_RELEASE = {
|
|
|
106
106
|
// 0.1.154 removes the short-lived generated enrich StepOptions recompute
|
|
107
107
|
// fields shipped in 0.1.153.
|
|
108
108
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
109
|
-
version: '0.1.
|
|
109
|
+
version: '0.1.214',
|
|
110
110
|
apiContract: '2026-06-dataset-handle-results-hard-cutover',
|
|
111
111
|
supportPolicy: {
|
|
112
|
-
latest: '0.1.
|
|
112
|
+
latest: '0.1.214',
|
|
113
113
|
minimumSupported: '0.1.53',
|
|
114
114
|
deprecatedBelow: '0.1.53',
|
|
115
115
|
commandMinimumSupported: [
|
|
@@ -1138,9 +1138,9 @@ export interface StartPlayRunRequest {
|
|
|
1138
1138
|
/** Optionally let the start request wait briefly and return a terminal result. */
|
|
1139
1139
|
waitForCompletionMs?: number;
|
|
1140
1140
|
/**
|
|
1141
|
-
* Per-run execution profile override. The server defaults to
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1141
|
+
* Per-run execution profile override. The server defaults to absurd;
|
|
1142
|
+
* callers that need workers_edge can pass it here. Most callers should leave
|
|
1143
|
+
* this unset.
|
|
1144
1144
|
*/
|
|
1145
1145
|
profile?: string;
|
|
1146
1146
|
/** Optional per-run provider execution mode for eval/smoke runs. */
|
|
@@ -207,11 +207,27 @@ type RuntimeApiRequest =
|
|
|
207
207
|
runId: string;
|
|
208
208
|
key: string;
|
|
209
209
|
}
|
|
210
|
+
| {
|
|
211
|
+
action: 'acquire_runtime_receipt_execution_lock';
|
|
212
|
+
playName: string;
|
|
213
|
+
runId: string;
|
|
214
|
+
key: string;
|
|
215
|
+
ownerExecutionId: string;
|
|
216
|
+
ttlMs: number;
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
action: 'release_runtime_receipt_execution_lock';
|
|
220
|
+
playName: string;
|
|
221
|
+
runId: string;
|
|
222
|
+
key: string;
|
|
223
|
+
ownerExecutionId: string;
|
|
224
|
+
}
|
|
210
225
|
| {
|
|
211
226
|
action: 'claim_runtime_step_receipt';
|
|
212
227
|
playName: string;
|
|
213
228
|
runId: string;
|
|
214
229
|
key: string;
|
|
230
|
+
leaseId?: string;
|
|
215
231
|
runAttempt?: number | null;
|
|
216
232
|
leaseAware?: boolean;
|
|
217
233
|
reclaimRunning?: boolean;
|
|
@@ -265,6 +281,7 @@ type RuntimeApiRequest =
|
|
|
265
281
|
playName: string;
|
|
266
282
|
runId: string;
|
|
267
283
|
keys: string[];
|
|
284
|
+
leaseIds?: string[];
|
|
268
285
|
runAttempt?: number | null;
|
|
269
286
|
leaseAware?: boolean;
|
|
270
287
|
reclaimRunning?: boolean;
|
|
@@ -1332,6 +1349,38 @@ export async function getRuntimeStepReceiptViaAppRuntime(
|
|
|
1332
1349
|
});
|
|
1333
1350
|
}
|
|
1334
1351
|
|
|
1352
|
+
export async function acquireRuntimeReceiptExecutionLockViaAppRuntime(
|
|
1353
|
+
context: WorkerRuntimeApiContext,
|
|
1354
|
+
input: Omit<
|
|
1355
|
+
Extract<
|
|
1356
|
+
RuntimeApiRequest,
|
|
1357
|
+
{ action: 'acquire_runtime_receipt_execution_lock' }
|
|
1358
|
+
>,
|
|
1359
|
+
'action'
|
|
1360
|
+
>,
|
|
1361
|
+
): Promise<{ ownerExecutionId: string; expiresAt: string } | null> {
|
|
1362
|
+
return await postAppRuntimeApi(context, {
|
|
1363
|
+
action: 'acquire_runtime_receipt_execution_lock',
|
|
1364
|
+
...input,
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export async function releaseRuntimeReceiptExecutionLockViaAppRuntime(
|
|
1369
|
+
context: WorkerRuntimeApiContext,
|
|
1370
|
+
input: Omit<
|
|
1371
|
+
Extract<
|
|
1372
|
+
RuntimeApiRequest,
|
|
1373
|
+
{ action: 'release_runtime_receipt_execution_lock' }
|
|
1374
|
+
>,
|
|
1375
|
+
'action'
|
|
1376
|
+
>,
|
|
1377
|
+
): Promise<boolean> {
|
|
1378
|
+
return await postAppRuntimeApi(context, {
|
|
1379
|
+
action: 'release_runtime_receipt_execution_lock',
|
|
1380
|
+
...input,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1335
1384
|
export async function getRuntimeStepReceiptsViaAppRuntime(
|
|
1336
1385
|
context: WorkerRuntimeApiContext,
|
|
1337
1386
|
input: Omit<
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlayCallExecution } from './play-call-execution';
|
|
2
|
+
|
|
3
|
+
export function resolveChildExecutionPlacement(input: {
|
|
4
|
+
requiresWorkflow: boolean;
|
|
5
|
+
execution?: PlayCallExecution | null;
|
|
6
|
+
}): { strategy: 'inline' | 'scheduled' } {
|
|
7
|
+
return {
|
|
8
|
+
strategy:
|
|
9
|
+
input.execution === 'child-workflow' ||
|
|
10
|
+
(input.execution !== 'inline' && input.requiresWorkflow)
|
|
11
|
+
? 'scheduled'
|
|
12
|
+
: 'inline',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
flattenStaticPipeline,
|
|
3
3
|
type PlayStaticPipeline,
|
|
4
4
|
} from '../plays/static-pipeline';
|
|
5
|
+
import type { PlayCallExecution } from './play-call-execution';
|
|
6
|
+
import { resolveChildExecutionPlacement } from './child-execution-placement';
|
|
5
7
|
|
|
6
8
|
export type ChildExecutionStrategy = 'inline' | 'scheduled';
|
|
7
9
|
|
|
@@ -12,7 +14,9 @@ export type ChildExecutionDecision = {
|
|
|
12
14
|
| 'missing_static_contract'
|
|
13
15
|
| 'dataset_child'
|
|
14
16
|
| 'event_wait_child'
|
|
15
|
-
| 'explicit_timeout'
|
|
17
|
+
| 'explicit_timeout'
|
|
18
|
+
| 'explicit_inline'
|
|
19
|
+
| 'explicit_child_workflow';
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -23,31 +27,61 @@ export type ChildExecutionDecision = {
|
|
|
23
27
|
export function resolveChildExecutionStrategy(input: {
|
|
24
28
|
pipeline: PlayStaticPipeline | null | undefined;
|
|
25
29
|
timeoutMs?: number | null;
|
|
30
|
+
execution?: PlayCallExecution | null;
|
|
31
|
+
childPlayName?: string | null;
|
|
26
32
|
}): ChildExecutionDecision {
|
|
27
|
-
if (input.
|
|
28
|
-
return { strategy: 'scheduled', reason: '
|
|
33
|
+
if (input.execution === 'child-workflow') {
|
|
34
|
+
return { strategy: 'scheduled', reason: 'explicit_child_workflow' };
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const steps = input.pipeline ? flattenStaticPipeline(input.pipeline) : [];
|
|
37
|
+
const authoredDecision: ChildExecutionDecision =
|
|
38
|
+
input.timeoutMs != null
|
|
39
|
+
? { strategy: 'scheduled', reason: 'explicit_timeout' }
|
|
40
|
+
: !input.pipeline
|
|
41
|
+
? { strategy: 'scheduled', reason: 'missing_static_contract' }
|
|
42
|
+
: input.pipeline.tableNamespace ||
|
|
43
|
+
input.pipeline.sheetContract ||
|
|
44
|
+
steps.some((step) => step.type === 'dataset' || step.type === 'csv')
|
|
45
|
+
? { strategy: 'scheduled', reason: 'dataset_child' }
|
|
46
|
+
: steps.some(
|
|
47
|
+
(step) =>
|
|
48
|
+
step.type === 'tool' &&
|
|
49
|
+
(step.isEventWait || step.toolId === 'test_wait_for_event'),
|
|
50
|
+
)
|
|
51
|
+
? { strategy: 'scheduled', reason: 'event_wait_child' }
|
|
52
|
+
: { strategy: 'inline', reason: 'scalar_child' };
|
|
53
|
+
if (input.execution === 'inline') {
|
|
54
|
+
if (
|
|
55
|
+
authoredDecision.strategy === 'scheduled' &&
|
|
56
|
+
authoredDecision.reason !== 'missing_static_contract'
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
inlineExecutionRequiresScalarChildMessage(
|
|
60
|
+
input.childPlayName?.trim() || 'child',
|
|
61
|
+
authoredDecision.reason,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return { strategy: 'inline', reason: 'explicit_inline' };
|
|
32
66
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
input.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return { strategy: 'scheduled', reason: 'dataset_child' };
|
|
67
|
+
const placement = resolveChildExecutionPlacement({
|
|
68
|
+
requiresWorkflow: authoredDecision.strategy === 'scheduled',
|
|
69
|
+
execution: input.execution,
|
|
70
|
+
});
|
|
71
|
+
if (placement.strategy === 'inline') {
|
|
72
|
+
return { strategy: 'inline', reason: 'scalar_child' };
|
|
40
73
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
return authoredDecision;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function inlineExecutionRequiresScalarChildMessage(
|
|
78
|
+
childPlayName: string,
|
|
79
|
+
reason: ChildExecutionDecision['reason'],
|
|
80
|
+
): string {
|
|
81
|
+
return (
|
|
82
|
+
`ctx.runPlay("${childPlayName}") requested execution: "inline", but the child requires a child workflow ` +
|
|
83
|
+
`(${reason}). Remove the explicit execution option or use execution: "child-workflow".`
|
|
84
|
+
);
|
|
51
85
|
}
|
|
52
86
|
|
|
53
87
|
/**
|
|
@@ -63,7 +97,7 @@ export function inlineChildScheduledInRowMessage(
|
|
|
63
97
|
return (
|
|
64
98
|
`ctx.runPlay("${childPlayName}") runs per row in the "${rowContextField}" batch, ` +
|
|
65
99
|
'but it is a scheduling-required child (it uses ctx.dataset()/ctx.csv(), waits on an ' +
|
|
66
|
-
'event,
|
|
100
|
+
'event, sets an explicit timeout, or explicitly requests a child workflow). Row-scoped children must be scalar. ' +
|
|
67
101
|
'Give the batch ONE dataset child at play level instead of fanning a scheduled run out per row.'
|
|
68
102
|
);
|
|
69
103
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A completed receipt is an immutable cache fact. Execution coordination is a
|
|
3
|
+
* separate, short-lived concern and must never be represented by a "running"
|
|
4
|
+
* receipt.
|
|
5
|
+
*/
|
|
6
|
+
export type CompletedReceipt<T> = {
|
|
7
|
+
key: string;
|
|
8
|
+
output: T;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
expiresAt?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CompletedReceiptCache<T> = {
|
|
14
|
+
get(key: string): Promise<CompletedReceipt<T> | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Insert only when the key is absent. On conflict, return the already stored
|
|
17
|
+
* immutable fact. Implementations should use INSERT ... ON CONFLICT DO
|
|
18
|
+
* NOTHING followed by a point read.
|
|
19
|
+
*/
|
|
20
|
+
putIfAbsent(receipt: CompletedReceipt<T>): Promise<CompletedReceipt<T>>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ReceiptExecutionLock = {
|
|
24
|
+
receiptKey: string;
|
|
25
|
+
ownerExecutionId: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ReceiptExecutionLockStore = {
|
|
30
|
+
tryAcquire(input: {
|
|
31
|
+
receiptKey: string;
|
|
32
|
+
ownerExecutionId: string;
|
|
33
|
+
expiresAt: string;
|
|
34
|
+
}): Promise<ReceiptExecutionLock | null>;
|
|
35
|
+
release(input: {
|
|
36
|
+
receiptKey: string;
|
|
37
|
+
ownerExecutionId: string;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ReceiptMissCoordination =
|
|
42
|
+
| { kind: 'provider_idempotency' }
|
|
43
|
+
| {
|
|
44
|
+
kind: 'execution_lock';
|
|
45
|
+
store: ReceiptExecutionLockStore;
|
|
46
|
+
ownerExecutionId: string;
|
|
47
|
+
lockTtlMs: number;
|
|
48
|
+
waitTimeoutMs: number;
|
|
49
|
+
pollIntervalMs?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class ReceiptExecutionLockTimeoutError extends Error {
|
|
53
|
+
constructor(receiptKey: string, waitTimeoutMs: number) {
|
|
54
|
+
super(
|
|
55
|
+
`Timed out waiting ${waitTimeoutMs}ms for receipt execution lock ${receiptKey}.`,
|
|
56
|
+
);
|
|
57
|
+
this.name = 'ReceiptExecutionLockTimeoutError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MAX_EXECUTION_LOCK_TTL_MS = 10 * 60_000;
|
|
62
|
+
|
|
63
|
+
function assertBoundedLockPolicy(
|
|
64
|
+
policy: Extract<ReceiptMissCoordination, { kind: 'execution_lock' }>,
|
|
65
|
+
): void {
|
|
66
|
+
if (
|
|
67
|
+
!Number.isFinite(policy.lockTtlMs) ||
|
|
68
|
+
policy.lockTtlMs <= 0 ||
|
|
69
|
+
policy.lockTtlMs > MAX_EXECUTION_LOCK_TTL_MS
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Receipt execution lock TTL must be between 1 and ${MAX_EXECUTION_LOCK_TTL_MS}ms.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(policy.waitTimeoutMs) || policy.waitTimeoutMs <= 0) {
|
|
76
|
+
throw new Error('Receipt execution lock wait timeout must be positive.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sleep(ms: number): Promise<void> {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function executeAndPublish<T>(input: {
|
|
85
|
+
receiptKey: string;
|
|
86
|
+
cache: CompletedReceiptCache<T>;
|
|
87
|
+
execute: () => Promise<T>;
|
|
88
|
+
now: () => number;
|
|
89
|
+
receiptTtlMs?: number | null;
|
|
90
|
+
}): Promise<T> {
|
|
91
|
+
const output = await input.execute();
|
|
92
|
+
const now = input.now();
|
|
93
|
+
const receipt = await input.cache.putIfAbsent({
|
|
94
|
+
key: input.receiptKey,
|
|
95
|
+
output,
|
|
96
|
+
createdAt: new Date(now).toISOString(),
|
|
97
|
+
...(input.receiptTtlMs
|
|
98
|
+
? { expiresAt: new Date(now + input.receiptTtlMs).toISOString() }
|
|
99
|
+
: {}),
|
|
100
|
+
});
|
|
101
|
+
return receipt.output;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute one semantic operation against an immutable completed-receipt cache.
|
|
106
|
+
*
|
|
107
|
+
* Providers with native idempotency do not need a Deepline execution lock:
|
|
108
|
+
* concurrent misses may dispatch with the same provider idempotency key, then
|
|
109
|
+
* converge on the first immutable cache insert. A separate temporary lock is
|
|
110
|
+
* reserved for actions explicitly classified as dangerous non-idempotent side
|
|
111
|
+
* effects. Ordinary enrichment misses may do bounded duplicate work and
|
|
112
|
+
* converge on the first published receipt.
|
|
113
|
+
*/
|
|
114
|
+
export async function executeWithCompletedReceiptCache<T>(input: {
|
|
115
|
+
receiptKey: string;
|
|
116
|
+
cache: CompletedReceiptCache<T>;
|
|
117
|
+
missCoordination: ReceiptMissCoordination;
|
|
118
|
+
execute: () => Promise<T>;
|
|
119
|
+
now?: () => number;
|
|
120
|
+
receiptTtlMs?: number | null;
|
|
121
|
+
}): Promise<T> {
|
|
122
|
+
const cached = await input.cache.get(input.receiptKey);
|
|
123
|
+
if (cached) return cached.output;
|
|
124
|
+
|
|
125
|
+
const now = input.now ?? Date.now;
|
|
126
|
+
if (input.missCoordination.kind === 'provider_idempotency') {
|
|
127
|
+
return await executeAndPublish({ ...input, now });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const policy = input.missCoordination;
|
|
131
|
+
assertBoundedLockPolicy(policy);
|
|
132
|
+
const deadline = now() + policy.waitTimeoutMs;
|
|
133
|
+
const pollIntervalMs = Math.max(1, policy.pollIntervalMs ?? 100);
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
const lock = await policy.store.tryAcquire({
|
|
137
|
+
receiptKey: input.receiptKey,
|
|
138
|
+
ownerExecutionId: policy.ownerExecutionId,
|
|
139
|
+
expiresAt: new Date(now() + policy.lockTtlMs).toISOString(),
|
|
140
|
+
});
|
|
141
|
+
if (lock) {
|
|
142
|
+
try {
|
|
143
|
+
// The previous owner may have published between our cache read and
|
|
144
|
+
// lock acquisition. Never execute after that terminal fact exists.
|
|
145
|
+
const afterAcquire = await input.cache.get(input.receiptKey);
|
|
146
|
+
if (afterAcquire) return afterAcquire.output;
|
|
147
|
+
return await executeAndPublish({ ...input, now });
|
|
148
|
+
} finally {
|
|
149
|
+
await policy.store.release({
|
|
150
|
+
receiptKey: input.receiptKey,
|
|
151
|
+
ownerExecutionId: policy.ownerExecutionId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const completed = await input.cache.get(input.receiptKey);
|
|
157
|
+
if (completed) return completed.output;
|
|
158
|
+
if (now() >= deadline) {
|
|
159
|
+
throw new ReceiptExecutionLockTimeoutError(
|
|
160
|
+
input.receiptKey,
|
|
161
|
+
policy.waitTimeoutMs,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - now())));
|
|
165
|
+
}
|
|
166
|
+
}
|