deepline 0.3.128 → 0.3.130
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/sdk/src/client.ts +34 -9
- package/dist/bundling-sources/sdk/src/http.ts +22 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
- package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
- package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +114 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +28 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +146 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +904 -77
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
- package/dist/cli/index.js +250 -38
- package/dist/cli/index.mjs +250 -38
- package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
- package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +231 -35
- package/dist/index.mjs +231 -35
- package/dist/install-integrity.json +31 -2
- 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 +65 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isIntegrationOperationValueReference,
|
|
5
|
+
type IntegrationOperationValueReference,
|
|
6
|
+
} from './integration-operation-value-reference';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The value-retirement journal needs only the scheduler client's query
|
|
10
|
+
* surface. Keeping that structural contract here lets the Fly gateway own its
|
|
11
|
+
* janitor without importing the application scheduler implementation.
|
|
12
|
+
*/
|
|
13
|
+
type PostgresSchedulerQueryClient = {
|
|
14
|
+
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
15
|
+
sql: string,
|
|
16
|
+
params?: unknown[],
|
|
17
|
+
): Promise<{ rows: Row[] }>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SCHEMA = 'runtime_scheduler';
|
|
21
|
+
|
|
22
|
+
function quoteIdent(value: string): string {
|
|
23
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tableName(schema: string): string {
|
|
27
|
+
return `${quoteIdent(schema)}.${quoteIdent(
|
|
28
|
+
'runtime_integration_operation_value_retirements',
|
|
29
|
+
)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function required(value: string, label: string): string {
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
if (!normalized) throw new Error(`${label} must be non-empty.`);
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function limit(value: number | undefined): number {
|
|
39
|
+
const normalized = value ?? 32;
|
|
40
|
+
if (!Number.isSafeInteger(normalized) || normalized < 1 || normalized > 256) {
|
|
41
|
+
throw new Error('limit must be a safe integer between 1 and 256.');
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type RetirementRow = {
|
|
47
|
+
org_id: string;
|
|
48
|
+
operation_key: string;
|
|
49
|
+
object_key: string;
|
|
50
|
+
object_digest: string;
|
|
51
|
+
reference_json: unknown;
|
|
52
|
+
claim_token: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ClaimedIntegrationOperationValueRetirement = {
|
|
56
|
+
orgId: string;
|
|
57
|
+
operationKey: string;
|
|
58
|
+
objectKey: string;
|
|
59
|
+
objectDigest: string;
|
|
60
|
+
reference: IntegrationOperationValueReference;
|
|
61
|
+
claimToken: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Claims a bounded set of detached immutable values. A claim remains durable
|
|
66
|
+
* until it is confirmed or released, so a process crash after object deletion
|
|
67
|
+
* cannot quietly permit a second writer to reuse the same deterministic key.
|
|
68
|
+
*/
|
|
69
|
+
export async function claimPostgresIntegrationOperationValueRetirements(input: {
|
|
70
|
+
client: PostgresSchedulerQueryClient;
|
|
71
|
+
schema?: string;
|
|
72
|
+
limit?: number;
|
|
73
|
+
claimToken?: string;
|
|
74
|
+
}): Promise<ClaimedIntegrationOperationValueRetirement[]> {
|
|
75
|
+
const schema = input.schema?.trim() || DEFAULT_SCHEMA;
|
|
76
|
+
const claimToken = required(input.claimToken ?? randomUUID(), 'claimToken');
|
|
77
|
+
const claimed = await input.client.query<RetirementRow>(
|
|
78
|
+
`WITH candidates AS (
|
|
79
|
+
SELECT ctid
|
|
80
|
+
FROM ${tableName(schema)}
|
|
81
|
+
WHERE (
|
|
82
|
+
state = 'pending'
|
|
83
|
+
OR (
|
|
84
|
+
state = 'deleting'
|
|
85
|
+
AND updated_at < now() - interval '15 minutes'
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
AND jsonb_typeof(reference_json) = 'object'
|
|
89
|
+
AND reference_json ->> 'storage' = 'r2'
|
|
90
|
+
AND reference_json ->> 'codec' IN ('json', 'json-v1')
|
|
91
|
+
AND NULLIF(reference_json ->> 'key', '') IS NOT NULL
|
|
92
|
+
AND (reference_json ->> 'digest') ~ '^[A-Fa-f0-9]{64}$'
|
|
93
|
+
ORDER BY updated_at ASC, object_key ASC
|
|
94
|
+
LIMIT $1
|
|
95
|
+
FOR UPDATE SKIP LOCKED
|
|
96
|
+
)
|
|
97
|
+
UPDATE ${tableName(schema)} AS retirements
|
|
98
|
+
SET state = 'deleting',
|
|
99
|
+
claim_token = $2,
|
|
100
|
+
updated_at = now()
|
|
101
|
+
FROM candidates
|
|
102
|
+
WHERE retirements.ctid = candidates.ctid
|
|
103
|
+
RETURNING retirements.org_id, retirements.operation_key,
|
|
104
|
+
retirements.object_key, retirements.object_digest,
|
|
105
|
+
retirements.reference_json, retirements.claim_token`,
|
|
106
|
+
[limit(input.limit), claimToken],
|
|
107
|
+
);
|
|
108
|
+
return claimed.rows.map((row) => {
|
|
109
|
+
if (!isIntegrationOperationValueReference(row.reference_json)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Integration Operation value retirement ${row.object_key} has an invalid reference.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
row.reference_json.key !== row.object_key ||
|
|
116
|
+
row.reference_json.digest.toLowerCase() !== row.object_digest.toLowerCase()
|
|
117
|
+
) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Integration Operation value retirement ${row.object_key} does not match its immutable reference.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
orgId: row.org_id,
|
|
124
|
+
operationKey: row.operation_key,
|
|
125
|
+
objectKey: row.object_key,
|
|
126
|
+
objectDigest: row.object_digest,
|
|
127
|
+
reference: row.reference_json,
|
|
128
|
+
claimToken: row.claim_token,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function confirmPostgresIntegrationOperationValueRetirement(input: {
|
|
134
|
+
client: PostgresSchedulerQueryClient;
|
|
135
|
+
schema?: string;
|
|
136
|
+
orgId: string;
|
|
137
|
+
objectKey: string;
|
|
138
|
+
objectDigest: string;
|
|
139
|
+
claimToken: string;
|
|
140
|
+
}): Promise<boolean> {
|
|
141
|
+
const schema = input.schema?.trim() || DEFAULT_SCHEMA;
|
|
142
|
+
const confirmed = await input.client.query<{ confirmed: number }>(
|
|
143
|
+
`UPDATE ${tableName(schema)}
|
|
144
|
+
SET state = 'deleted',
|
|
145
|
+
claim_token = NULL,
|
|
146
|
+
deletion_confirmed_at = now(),
|
|
147
|
+
updated_at = now()
|
|
148
|
+
WHERE org_id = $1
|
|
149
|
+
AND object_key = $2
|
|
150
|
+
AND object_digest = $3
|
|
151
|
+
AND state = 'deleting'
|
|
152
|
+
AND claim_token = $4
|
|
153
|
+
RETURNING 1 AS confirmed`,
|
|
154
|
+
[
|
|
155
|
+
required(input.orgId, 'orgId'),
|
|
156
|
+
required(input.objectKey, 'objectKey'),
|
|
157
|
+
required(input.objectDigest, 'objectDigest').toLowerCase(),
|
|
158
|
+
required(input.claimToken, 'claimToken'),
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
return confirmed.rows.length === 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Release an unsuccessful delete so a later bounded janitor pass can retry. */
|
|
165
|
+
export async function releasePostgresIntegrationOperationValueRetirement(input: {
|
|
166
|
+
client: PostgresSchedulerQueryClient;
|
|
167
|
+
schema?: string;
|
|
168
|
+
orgId: string;
|
|
169
|
+
objectKey: string;
|
|
170
|
+
objectDigest: string;
|
|
171
|
+
claimToken: string;
|
|
172
|
+
}): Promise<boolean> {
|
|
173
|
+
const schema = input.schema?.trim() || DEFAULT_SCHEMA;
|
|
174
|
+
const released = await input.client.query<{ released: number }>(
|
|
175
|
+
`UPDATE ${tableName(schema)}
|
|
176
|
+
SET state = 'pending',
|
|
177
|
+
claim_token = NULL,
|
|
178
|
+
updated_at = now()
|
|
179
|
+
WHERE org_id = $1
|
|
180
|
+
AND object_key = $2
|
|
181
|
+
AND object_digest = $3
|
|
182
|
+
AND state = 'deleting'
|
|
183
|
+
AND claim_token = $4
|
|
184
|
+
RETURNING 1 AS released`,
|
|
185
|
+
[
|
|
186
|
+
required(input.orgId, 'orgId'),
|
|
187
|
+
required(input.objectKey, 'objectKey'),
|
|
188
|
+
required(input.objectDigest, 'objectDigest').toLowerCase(),
|
|
189
|
+
required(input.claimToken, 'claimToken'),
|
|
190
|
+
],
|
|
191
|
+
);
|
|
192
|
+
return released.rows.length === 1;
|
|
193
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
IntegrationOperationValueReadError,
|
|
4
|
+
isIntegrationOperationValueReference,
|
|
5
|
+
type IntegrationOperationValueReference,
|
|
6
|
+
} from './integration-operation-value-reference';
|
|
7
|
+
import type { IntegrationOperationValueReader } from './integration-operation-store-contract';
|
|
8
|
+
import { BoundedBatchFunction } from '../bounded-batch-function';
|
|
9
|
+
import {
|
|
10
|
+
requireRuntimeValueBlobStore,
|
|
11
|
+
type RuntimeValueBlobStore,
|
|
12
|
+
} from './runtime-receipt-value-storage';
|
|
13
|
+
|
|
14
|
+
/** Keep small canonical outcomes in the transaction row. */
|
|
15
|
+
export const INTEGRATION_OPERATION_VALUE_INLINE_MAX_BYTES = 256 * 1024;
|
|
16
|
+
|
|
17
|
+
export type PersistedIntegrationOperationValue =
|
|
18
|
+
| { kind: 'inline' }
|
|
19
|
+
| { kind: 'reference'; outputRef: IntegrationOperationValueReference };
|
|
20
|
+
|
|
21
|
+
export type PreparedIntegrationOperationValue =
|
|
22
|
+
| { kind: 'inline' }
|
|
23
|
+
| {
|
|
24
|
+
kind: 'reference';
|
|
25
|
+
outputRef: IntegrationOperationValueReference;
|
|
26
|
+
/** Private until the operation store has reserved its durable owner. */
|
|
27
|
+
body: Buffer;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type IntegrationOperationValueInput = {
|
|
31
|
+
orgId: string;
|
|
32
|
+
operationKey: string;
|
|
33
|
+
output: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A prepared value separates deterministic serialization from object I/O.
|
|
38
|
+
* Gateway composition can therefore fence pending ownership before handing a
|
|
39
|
+
* large body to the shared R2 admission queue.
|
|
40
|
+
*/
|
|
41
|
+
export type IntegrationOperationValuePersister = ((
|
|
42
|
+
input: IntegrationOperationValueInput,
|
|
43
|
+
) => Promise<PersistedIntegrationOperationValue>) & {
|
|
44
|
+
prepare(input: IntegrationOperationValueInput): PreparedIntegrationOperationValue;
|
|
45
|
+
persistPrepared(
|
|
46
|
+
input: PreparedIntegrationOperationValue,
|
|
47
|
+
): Promise<PersistedIntegrationOperationValue>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type IntegrationOperationValueRetirer = (input: {
|
|
51
|
+
reference: IntegrationOperationValueReference;
|
|
52
|
+
}) => Promise<void>;
|
|
53
|
+
|
|
54
|
+
function sha256(value: string | Buffer): string {
|
|
55
|
+
return createHash('sha256').update(value).digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nonempty(value: string, label: string): string {
|
|
59
|
+
const normalized = value.trim();
|
|
60
|
+
if (!normalized) throw new Error(`${label} must be non-empty.`);
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nonnegativeInteger(value: number, label: string): number {
|
|
65
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
66
|
+
throw new Error(`${label} must be a non-negative safe integer.`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function positiveInteger(value: number, label: string): number {
|
|
72
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
73
|
+
throw new Error(`${label} must be a positive safe integer.`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function serializeOutput(output: unknown): string {
|
|
79
|
+
const serialized = JSON.stringify(output);
|
|
80
|
+
if (typeof serialized !== 'string') {
|
|
81
|
+
throw new Error('Integration Operation output must be JSON serializable.');
|
|
82
|
+
}
|
|
83
|
+
return serialized;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bindings(input: { orgId: string; operationKey: string }): {
|
|
87
|
+
orgBinding: string;
|
|
88
|
+
operationBinding: string;
|
|
89
|
+
} {
|
|
90
|
+
return {
|
|
91
|
+
orgBinding: sha256(
|
|
92
|
+
`runtime-integration-operation-value/org/v1/${input.orgId}`,
|
|
93
|
+
),
|
|
94
|
+
operationBinding: sha256(
|
|
95
|
+
`runtime-integration-operation-value/operation/v1/${input.orgId}\0${input.operationKey}`,
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function integrationOperationValueStorageKey(input: {
|
|
101
|
+
orgId: string;
|
|
102
|
+
operationKey: string;
|
|
103
|
+
digest: string;
|
|
104
|
+
}): string {
|
|
105
|
+
const orgId = nonempty(input.orgId, 'orgId');
|
|
106
|
+
const operationKey = nonempty(input.operationKey, 'operationKey');
|
|
107
|
+
if (!/^[a-f0-9]{64}$/i.test(input.digest)) {
|
|
108
|
+
throw new Error('digest must be a SHA-256 hex digest.');
|
|
109
|
+
}
|
|
110
|
+
const { orgBinding, operationBinding } = bindings({ orgId, operationKey });
|
|
111
|
+
return `runtime/integration-operation-values/v1/${orgBinding}/${operationBinding}/${input.digest}.json`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function referenceFor(input: {
|
|
115
|
+
orgId: string;
|
|
116
|
+
operationKey: string;
|
|
117
|
+
body: Buffer;
|
|
118
|
+
}): IntegrationOperationValueReference {
|
|
119
|
+
const orgId = nonempty(input.orgId, 'orgId');
|
|
120
|
+
const operationKey = nonempty(input.operationKey, 'operationKey');
|
|
121
|
+
const digest = sha256(input.body);
|
|
122
|
+
const { orgBinding, operationBinding } = bindings({ orgId, operationKey });
|
|
123
|
+
return {
|
|
124
|
+
storage: 'r2',
|
|
125
|
+
codec: 'json',
|
|
126
|
+
key: integrationOperationValueStorageKey({ orgId, operationKey, digest }),
|
|
127
|
+
digest,
|
|
128
|
+
byteLength: input.body.byteLength,
|
|
129
|
+
orgBinding,
|
|
130
|
+
operationBinding,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Serialize a value and derive its immutable reference without touching R2. */
|
|
135
|
+
export function prepareIntegrationOperationValue(input: {
|
|
136
|
+
orgId: string;
|
|
137
|
+
operationKey: string;
|
|
138
|
+
output: unknown;
|
|
139
|
+
inlineMaxBytes?: number;
|
|
140
|
+
}): PreparedIntegrationOperationValue {
|
|
141
|
+
const orgId = nonempty(input.orgId, 'orgId');
|
|
142
|
+
const operationKey = nonempty(input.operationKey, 'operationKey');
|
|
143
|
+
const body = Buffer.from(serializeOutput(input.output), 'utf8');
|
|
144
|
+
const inlineMaxBytes = positiveInteger(
|
|
145
|
+
input.inlineMaxBytes ?? INTEGRATION_OPERATION_VALUE_INLINE_MAX_BYTES,
|
|
146
|
+
'inlineMaxBytes',
|
|
147
|
+
);
|
|
148
|
+
if (body.byteLength <= inlineMaxBytes) return { kind: 'inline' };
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
kind: 'reference',
|
|
152
|
+
outputRef: referenceFor({ orgId, operationKey, body }),
|
|
153
|
+
body,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Write a prepared immutable body. A lost PUT acknowledgement observes the
|
|
159
|
+
* deterministic object; it never becomes permission to repeat provider work.
|
|
160
|
+
*/
|
|
161
|
+
export async function persistPreparedIntegrationOperationValue(input: {
|
|
162
|
+
value: PreparedIntegrationOperationValue;
|
|
163
|
+
store?: RuntimeValueBlobStore;
|
|
164
|
+
}): Promise<PersistedIntegrationOperationValue> {
|
|
165
|
+
if (input.value.kind === 'inline') return input.value;
|
|
166
|
+
|
|
167
|
+
const store = requireRuntimeValueBlobStore(input.store);
|
|
168
|
+
try {
|
|
169
|
+
await store.put({
|
|
170
|
+
key: input.value.outputRef.key,
|
|
171
|
+
body: input.value.body,
|
|
172
|
+
contentType: 'application/json',
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (!(await store.exists(input.value.outputRef.key))) throw error;
|
|
176
|
+
}
|
|
177
|
+
return { kind: 'reference', outputRef: input.value.outputRef };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Convenience path for callers without an operation-store ownership fence.
|
|
182
|
+
* The gateway uses prepare + reserve + persistPrepared instead.
|
|
183
|
+
*/
|
|
184
|
+
export async function persistIntegrationOperationValue(input: {
|
|
185
|
+
orgId: string;
|
|
186
|
+
operationKey: string;
|
|
187
|
+
output: unknown;
|
|
188
|
+
inlineMaxBytes?: number;
|
|
189
|
+
store?: RuntimeValueBlobStore;
|
|
190
|
+
}): Promise<PersistedIntegrationOperationValue> {
|
|
191
|
+
return await persistPreparedIntegrationOperationValue({
|
|
192
|
+
value: prepareIntegrationOperationValue(input),
|
|
193
|
+
...(input.store ? { store: input.store } : {}),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One process-scoped admission budget for Integration Operation object PUTs.
|
|
199
|
+
* R2 has no multi-PUT; batching bounds retained work and concurrent physical
|
|
200
|
+
* requests without claiming a nonexistent storage transaction.
|
|
201
|
+
*/
|
|
202
|
+
export function createIntegrationOperationValuePersister(
|
|
203
|
+
input: {
|
|
204
|
+
store?: RuntimeValueBlobStore;
|
|
205
|
+
inlineMaxBytes?: number;
|
|
206
|
+
} = {},
|
|
207
|
+
): IntegrationOperationValuePersister {
|
|
208
|
+
const inlineMaxBytes =
|
|
209
|
+
input.inlineMaxBytes ?? INTEGRATION_OPERATION_VALUE_INLINE_MAX_BYTES;
|
|
210
|
+
type Write = Extract<PreparedIntegrationOperationValue, { kind: 'reference' }>;
|
|
211
|
+
const writes = new BoundedBatchFunction<
|
|
212
|
+
Write,
|
|
213
|
+
PersistedIntegrationOperationValue,
|
|
214
|
+
'r2'
|
|
215
|
+
>({
|
|
216
|
+
keyId: () => 'r2',
|
|
217
|
+
estimateBytes: (value) => value.body.byteLength,
|
|
218
|
+
maxBatchItems: 4,
|
|
219
|
+
maxBatchBytes: 12 * 1024 * 1024,
|
|
220
|
+
maxBatchUnits: 4,
|
|
221
|
+
maxRetainedItems: 32,
|
|
222
|
+
maxRetainedBytes: 24 * 1024 * 1024,
|
|
223
|
+
maxRetainedUnits: 32,
|
|
224
|
+
maxInFlightBatches: 2,
|
|
225
|
+
flushMs: 0,
|
|
226
|
+
execute: async ({ values }) =>
|
|
227
|
+
await Promise.all(
|
|
228
|
+
values.map(
|
|
229
|
+
async (value) =>
|
|
230
|
+
await persistPreparedIntegrationOperationValue({
|
|
231
|
+
value,
|
|
232
|
+
...(input.store ? { store: input.store } : {}),
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
});
|
|
237
|
+
const persistPrepared = async (
|
|
238
|
+
value: PreparedIntegrationOperationValue,
|
|
239
|
+
): Promise<PersistedIntegrationOperationValue> => {
|
|
240
|
+
if (value.kind === 'inline') return value;
|
|
241
|
+
return await writes.submit({ key: 'r2', value });
|
|
242
|
+
};
|
|
243
|
+
const persister = (async (value: IntegrationOperationValueInput) =>
|
|
244
|
+
await persistPrepared(
|
|
245
|
+
prepareIntegrationOperationValue({ ...value, inlineMaxBytes }),
|
|
246
|
+
)) as IntegrationOperationValuePersister;
|
|
247
|
+
persister.prepare = (value) =>
|
|
248
|
+
prepareIntegrationOperationValue({ ...value, inlineMaxBytes });
|
|
249
|
+
persister.persistPrepared = persistPrepared;
|
|
250
|
+
return persister;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Delete an immutable value only after its durable retirement claim is held.
|
|
255
|
+
* A successful DELETE acknowledgement is insufficient: object stores can lose
|
|
256
|
+
* it, so absence must be observed before the caller records confirmation.
|
|
257
|
+
*/
|
|
258
|
+
export async function deleteIntegrationOperationValue(input: {
|
|
259
|
+
reference: IntegrationOperationValueReference;
|
|
260
|
+
store?: RuntimeValueBlobStore;
|
|
261
|
+
}): Promise<void> {
|
|
262
|
+
if (!isIntegrationOperationValueReference(input.reference)) {
|
|
263
|
+
throw new Error('Integration Operation value deletion has an invalid reference.');
|
|
264
|
+
}
|
|
265
|
+
const store = requireRuntimeValueBlobStore(input.store);
|
|
266
|
+
try {
|
|
267
|
+
await store.remove(input.reference.key);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (await store.exists(input.reference.key)) throw error;
|
|
270
|
+
}
|
|
271
|
+
if (await store.exists(input.reference.key)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
'Integration Operation value deletion was not confirmed absent from object storage.',
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** One process-scoped bounded admission budget for immutable R2 DELETEs. */
|
|
279
|
+
export function createIntegrationOperationValueRetirer(
|
|
280
|
+
input: { store?: RuntimeValueBlobStore } = {},
|
|
281
|
+
): IntegrationOperationValueRetirer {
|
|
282
|
+
const deletes = new BoundedBatchFunction<
|
|
283
|
+
IntegrationOperationValueReference,
|
|
284
|
+
void,
|
|
285
|
+
'r2'
|
|
286
|
+
>({
|
|
287
|
+
keyId: () => 'r2',
|
|
288
|
+
estimateBytes: (value) => value.byteLength,
|
|
289
|
+
maxBatchItems: 4,
|
|
290
|
+
maxBatchBytes: 12 * 1024 * 1024,
|
|
291
|
+
maxBatchUnits: 4,
|
|
292
|
+
maxRetainedItems: 32,
|
|
293
|
+
maxRetainedBytes: 24 * 1024 * 1024,
|
|
294
|
+
maxRetainedUnits: 32,
|
|
295
|
+
maxInFlightBatches: 2,
|
|
296
|
+
flushMs: 0,
|
|
297
|
+
execute: async ({ values }) =>
|
|
298
|
+
await Promise.all(
|
|
299
|
+
values.map(
|
|
300
|
+
async (reference) =>
|
|
301
|
+
await deleteIntegrationOperationValue({
|
|
302
|
+
reference,
|
|
303
|
+
...(input.store ? { store: input.store } : {}),
|
|
304
|
+
}),
|
|
305
|
+
),
|
|
306
|
+
),
|
|
307
|
+
});
|
|
308
|
+
return async ({ reference }) => await deletes.submit({ key: 'r2', value: reference });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function assertScope(input: {
|
|
312
|
+
orgId: string;
|
|
313
|
+
operationKey: string;
|
|
314
|
+
reference: IntegrationOperationValueReference;
|
|
315
|
+
}): void {
|
|
316
|
+
const orgId = nonempty(input.orgId, 'orgId');
|
|
317
|
+
const operationKey = nonempty(input.operationKey, 'operationKey');
|
|
318
|
+
const expected = bindings({ orgId, operationKey });
|
|
319
|
+
if (
|
|
320
|
+
input.reference.orgBinding !== expected.orgBinding ||
|
|
321
|
+
input.reference.operationBinding !== expected.operationBinding ||
|
|
322
|
+
input.reference.key !==
|
|
323
|
+
integrationOperationValueStorageKey({
|
|
324
|
+
orgId,
|
|
325
|
+
operationKey,
|
|
326
|
+
digest: input.reference.digest,
|
|
327
|
+
})
|
|
328
|
+
) {
|
|
329
|
+
throw new IntegrationOperationValueReadError(
|
|
330
|
+
'scope_mismatch',
|
|
331
|
+
'Integration Operation value reference is outside the authenticated operation scope.',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isMissing(error: unknown): boolean {
|
|
337
|
+
if (!error || typeof error !== 'object') return false;
|
|
338
|
+
const record = error as Record<string, unknown>;
|
|
339
|
+
const code = String(record.code ?? record.name ?? '');
|
|
340
|
+
if (code === 'NoSuchKey' || code === 'NotFound') return true;
|
|
341
|
+
const metadata = record.$metadata;
|
|
342
|
+
return (
|
|
343
|
+
Boolean(metadata) &&
|
|
344
|
+
typeof metadata === 'object' &&
|
|
345
|
+
(metadata as Record<string, unknown>).httpStatusCode === 404
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function readIntegrationOperationValue(input: {
|
|
350
|
+
orgId: string;
|
|
351
|
+
operationKey: string;
|
|
352
|
+
reference: IntegrationOperationValueReference;
|
|
353
|
+
store?: RuntimeValueBlobStore;
|
|
354
|
+
}): Promise<unknown> {
|
|
355
|
+
assertScope(input);
|
|
356
|
+
const store = requireRuntimeValueBlobStore(input.store);
|
|
357
|
+
let body: Buffer;
|
|
358
|
+
try {
|
|
359
|
+
body = await store.get(input.reference.key);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new IntegrationOperationValueReadError(
|
|
362
|
+
isMissing(error) ? 'missing' : 'unavailable',
|
|
363
|
+
`Integration Operation value could not be read: ${
|
|
364
|
+
error instanceof Error ? error.message : 'unknown storage error'
|
|
365
|
+
}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
body.byteLength !==
|
|
370
|
+
nonnegativeInteger(input.reference.byteLength, 'byteLength') ||
|
|
371
|
+
sha256(body) !== input.reference.digest
|
|
372
|
+
) {
|
|
373
|
+
throw new IntegrationOperationValueReadError(
|
|
374
|
+
'corrupt',
|
|
375
|
+
'Integration Operation value failed its stored size or digest check.',
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
return JSON.parse(body.toString('utf8')) as unknown;
|
|
380
|
+
} catch {
|
|
381
|
+
throw new IntegrationOperationValueReadError(
|
|
382
|
+
'corrupt',
|
|
383
|
+
'Integration Operation value is not valid JSON for its declared codec.',
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** One process-scoped bounded admission budget for immutable R2 GETs. */
|
|
389
|
+
export function createIntegrationOperationValueReader(
|
|
390
|
+
input: { store?: RuntimeValueBlobStore } = {},
|
|
391
|
+
): IntegrationOperationValueReader {
|
|
392
|
+
type Read = {
|
|
393
|
+
orgId: string;
|
|
394
|
+
operationKey: string;
|
|
395
|
+
reference: IntegrationOperationValueReference;
|
|
396
|
+
};
|
|
397
|
+
const reads = new BoundedBatchFunction<Read, unknown, 'r2'>({
|
|
398
|
+
keyId: () => 'r2',
|
|
399
|
+
estimateBytes: (value) => value.reference.byteLength,
|
|
400
|
+
maxBatchItems: 4,
|
|
401
|
+
maxBatchBytes: 12 * 1024 * 1024,
|
|
402
|
+
maxBatchUnits: 4,
|
|
403
|
+
maxRetainedItems: 32,
|
|
404
|
+
maxRetainedBytes: 24 * 1024 * 1024,
|
|
405
|
+
maxRetainedUnits: 32,
|
|
406
|
+
maxInFlightBatches: 2,
|
|
407
|
+
flushMs: 0,
|
|
408
|
+
execute: async ({ values }) =>
|
|
409
|
+
await Promise.all(
|
|
410
|
+
values.map(
|
|
411
|
+
async (value) =>
|
|
412
|
+
await readIntegrationOperationValue({
|
|
413
|
+
...value,
|
|
414
|
+
...(input.store ? { store: input.store } : {}),
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
});
|
|
419
|
+
return async (value) => await reads.submit({ key: 'r2', value });
|
|
420
|
+
}
|