deepline 0.3.113 → 0.3.114
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 +3 -1
- package/dist/bundling-sources/sdk/src/compat.ts +167 -11
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/skills-version.ts +27 -1
- package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +6 -2
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +291 -6
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +24 -24
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +2 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +29 -1
- package/dist/bundling-sources/shared_libs/play-runtime/admission-transport-policy.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/direct-admission-deadline.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +325 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +134 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-admission-probe.ts +32 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-webhook-identity.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-cron-fire-task.ts +100 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +313 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +111 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +67 -8
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1394 -22
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +74 -13
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +79 -15
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +21 -21
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +4 -1
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +64 -0
- package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +20 -1
- package/dist/bundling-sources/shared_libs/security/receipt-admission-token.ts +224 -0
- package/dist/cli/index.js +574 -114
- package/dist/cli/index.mjs +592 -133
- package/dist/index.d.mts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +8 -3
- package/dist/index.mjs +8 -3
- package/dist/install-integrity.json +7 -0
- package/dist/plays/bundle-play-file.mjs +1 -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
|
@@ -34,11 +34,24 @@ const RECOVERY_FAILURE_RETRY_MAX_MS = 60_000;
|
|
|
34
34
|
export const NOTIFY_UNAVAILABLE_SAFETY_WAKE_MS = 1_000;
|
|
35
35
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* One projector can keep this many independent Run Ledger deliveries in
|
|
38
|
+
* flight after it has found durable work. Each delivery owns only short
|
|
39
|
+
* Postgres checkouts: claim, token-context read, and acknowledgement. It does
|
|
40
|
+
* not hold a scheduler connection across either gateway HTTP request. This is
|
|
41
|
+
* therefore a network/projection concurrency budget, deliberately independent
|
|
42
|
+
* of the projector's much smaller Postgres pool.
|
|
43
|
+
*
|
|
44
|
+
* A single successful probe starts a bounded wave; an idle projector still
|
|
45
|
+
* performs only one indexed head read per safety wake. `SKIP LOCKED` leases
|
|
46
|
+
* distinct runs, retaining strict causal ordering within every run.
|
|
40
47
|
*/
|
|
41
|
-
|
|
48
|
+
// `run.created` has one event per admitted Run, so a burst must project
|
|
49
|
+
// independently rather than wait for execution. Each Fly delivery first asks
|
|
50
|
+
// the launch-pinned Vercel deployment for a lease-fenced executor token,
|
|
51
|
+
// though. Keep that remote fan-out inside the Vercel control-plane budget:
|
|
52
|
+
// the Fly checkout is short, but 128 simultaneous token requests previously
|
|
53
|
+
// saturated the shared serverless scheduler pool before projection began.
|
|
54
|
+
export const RUN_LEDGER_OUTBOX_RECOVERY_MAX_IN_FLIGHT = 32;
|
|
42
55
|
|
|
43
56
|
type RecoveryTimer = ReturnType<typeof setTimeout>;
|
|
44
57
|
|
|
@@ -226,12 +239,33 @@ export function startRunLedgerOutboxRecovery(
|
|
|
226
239
|
timer.unref?.();
|
|
227
240
|
};
|
|
228
241
|
|
|
229
|
-
const drain = async (
|
|
242
|
+
const drain = async (reason: string): Promise<void> => {
|
|
230
243
|
while (requested && !closed) {
|
|
231
244
|
requested = false;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
// Probe first so an idle projector issues one small indexed read per
|
|
246
|
+
// wake rather than `parallelism` identical empty scans. Once the probe
|
|
247
|
+
// claims a row, the remaining slots can safely lease unrelated runs;
|
|
248
|
+
// each dispatcher releases its database client before gateway I/O.
|
|
249
|
+
let first: PostgresSchedulerOutboxProjectionResult;
|
|
250
|
+
try {
|
|
251
|
+
first = await deps.dispatchOnce();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
scheduleFailureRetry('dispatch', error);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const settled = [
|
|
257
|
+
{
|
|
258
|
+
status: 'fulfilled' as const,
|
|
259
|
+
value: first,
|
|
260
|
+
},
|
|
261
|
+
...(first.attempted === 0
|
|
262
|
+
? []
|
|
263
|
+
: await Promise.allSettled(
|
|
264
|
+
Array.from({ length: parallelism - 1 }, () =>
|
|
265
|
+
deps.dispatchOnce(),
|
|
266
|
+
),
|
|
267
|
+
)),
|
|
268
|
+
];
|
|
235
269
|
const rejected = settled.find(
|
|
236
270
|
(result): result is PromiseRejectedResult =>
|
|
237
271
|
result.status === 'rejected',
|
|
@@ -251,6 +285,18 @@ export function startRunLedgerOutboxRecovery(
|
|
|
251
285
|
(total, result) => total + result.retrying,
|
|
252
286
|
0,
|
|
253
287
|
);
|
|
288
|
+
const attempted = results.reduce(
|
|
289
|
+
(total, result) => total + result.attempted,
|
|
290
|
+
0,
|
|
291
|
+
);
|
|
292
|
+
const projected = results.reduce(
|
|
293
|
+
(total, result) => total + result.projected,
|
|
294
|
+
0,
|
|
295
|
+
);
|
|
296
|
+
const blocked = results.reduce(
|
|
297
|
+
(total, result) => total + result.failed,
|
|
298
|
+
0,
|
|
299
|
+
);
|
|
254
300
|
|
|
255
301
|
if (retrying > 0) {
|
|
256
302
|
// The failed row owns its own durable retry clock. Do NOT stop this
|
|
@@ -263,11 +309,25 @@ export function startRunLedgerOutboxRecovery(
|
|
|
263
309
|
failureCount = 0;
|
|
264
310
|
}
|
|
265
311
|
|
|
312
|
+
if (attempted > 0) {
|
|
313
|
+
// This is the durable queue's high-signal throughput event. It makes
|
|
314
|
+
// a delayed public Run observable as a projector/outbox problem,
|
|
315
|
+
// without emitting one noisy log line per normal database checkout.
|
|
316
|
+
logger.info('[run-ledger-outbox.recovery_wave]', {
|
|
317
|
+
reason,
|
|
318
|
+
parallelism,
|
|
319
|
+
attempted,
|
|
320
|
+
projected,
|
|
321
|
+
retrying,
|
|
322
|
+
blocked,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
266
326
|
// Continue until no event was claimable. A transient failure schedules
|
|
267
327
|
// its own durable retry, so the next pass naturally skips that run while
|
|
268
328
|
// moving unrelated runs forward. A permanent failure is settled once and
|
|
269
329
|
// likewise cannot block another run's causal prefix.
|
|
270
|
-
if (
|
|
330
|
+
if (attempted > 0) {
|
|
271
331
|
requested = true;
|
|
272
332
|
continue;
|
|
273
333
|
}
|
|
@@ -324,10 +384,11 @@ export function startRunLedgerOutboxRecoveryController(input: {
|
|
|
324
384
|
{ tokenAuthority: 'gateway', limit: 8 },
|
|
325
385
|
schedulerOptions,
|
|
326
386
|
),
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
387
|
+
// `dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope` releases each
|
|
388
|
+
// short scheduler checkout before it calls the Vercel gateway. Keep the
|
|
389
|
+
// durable projection wave large enough for a webhook burst without
|
|
390
|
+
// expanding the process' Postgres connection budget.
|
|
391
|
+
parallelism: RUN_LEDGER_OUTBOX_RECOVERY_MAX_IN_FLIGHT,
|
|
331
392
|
readNextDueAt: () =>
|
|
332
393
|
withRuntimeSchedulerClient(input.poolOptions, (client) =>
|
|
333
394
|
readPostgresSchedulerOutboxNextDueAt(client, schedulerOptions),
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Process-local admission before the atomic Postgres launch transaction.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* An ordinary Vercel launch holds one submit permit. The private Workpool
|
|
5
|
+
* batch adapter may widen that permit to the already-fenced request pool
|
|
6
|
+
* capacity minus one reserved control-plane client. The request still owns a
|
|
7
|
+
* single global transport leader, so this does not turn customer webhook
|
|
8
|
+
* bursts into unbounded PgBouncer connections.
|
|
8
9
|
*
|
|
9
10
|
* This is intentionally not durable state and does not acknowledge a launch.
|
|
10
11
|
* The existing submit transaction remains the only admission boundary. If the
|
|
@@ -23,6 +24,13 @@ export const RUNTIME_SCHEDULER_SUBMIT_ADMISSION = {
|
|
|
23
24
|
maxWaitMs: 60_000,
|
|
24
25
|
} as const;
|
|
25
26
|
|
|
27
|
+
// The private Vercel batch has at most 256 source lifecycles in flight. It
|
|
28
|
+
// may therefore need to park 225 of them behind its 31 reserved scheduler
|
|
29
|
+
// submit clients. Keep the per-process override at that same finite boundary:
|
|
30
|
+
// it is a backpressure buffer, not another durable transport or an unlimited
|
|
31
|
+
// way to consume PgBouncer connections.
|
|
32
|
+
const MAX_RUNTIME_SCHEDULER_SUBMIT_ADMISSION_QUEUED = 256;
|
|
33
|
+
|
|
26
34
|
type Waiter = {
|
|
27
35
|
enqueuedAt: number;
|
|
28
36
|
resolve: (permit: RuntimeSchedulerSubmitAdmissionPermit) => void;
|
|
@@ -32,6 +40,8 @@ type Waiter = {
|
|
|
32
40
|
|
|
33
41
|
type AdmissionEntry = {
|
|
34
42
|
active: number;
|
|
43
|
+
maxActive: number;
|
|
44
|
+
maxQueued: number;
|
|
35
45
|
waiters: Waiter[];
|
|
36
46
|
};
|
|
37
47
|
|
|
@@ -54,6 +64,18 @@ export type RuntimeSchedulerSubmitAdmissionParams = {
|
|
|
54
64
|
/** Kept in-memory only. Do not log this value because it contains DB auth. */
|
|
55
65
|
schedulerUrl: string;
|
|
56
66
|
schedulerSchema: string | null | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The private Vercel batch adapter may widen the local submitter to its
|
|
69
|
+
* bounded scheduler pool. Ordinary launchers omit this and retain one-at-a-
|
|
70
|
+
* time admission.
|
|
71
|
+
*/
|
|
72
|
+
maxActive?: number;
|
|
73
|
+
/**
|
|
74
|
+
* A private, already-bounded batch may widen its local wait buffer to its
|
|
75
|
+
* lifecycle budget. Ordinary launchers omit this and retain the 128-entry
|
|
76
|
+
* transient queue.
|
|
77
|
+
*/
|
|
78
|
+
maxQueued?: number;
|
|
57
79
|
};
|
|
58
80
|
|
|
59
81
|
export type RuntimeSchedulerSubmitAdmissionPermit = {
|
|
@@ -79,6 +101,37 @@ function pruneEntry(key: string, entry: AdmissionEntry): void {
|
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
function maxActiveFor(params: RuntimeSchedulerSubmitAdmissionParams): number {
|
|
105
|
+
const maxActive =
|
|
106
|
+
params.maxActive ?? RUNTIME_SCHEDULER_SUBMIT_ADMISSION.maxActive;
|
|
107
|
+
if (!Number.isInteger(maxActive) || maxActive < 1 || maxActive > 32) {
|
|
108
|
+
throw new Error('Runtime scheduler submit admission capacity is invalid.');
|
|
109
|
+
}
|
|
110
|
+
return maxActive;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function maxQueuedFor(params: RuntimeSchedulerSubmitAdmissionParams): number {
|
|
114
|
+
const maxQueued =
|
|
115
|
+
params.maxQueued ?? RUNTIME_SCHEDULER_SUBMIT_ADMISSION.maxQueued;
|
|
116
|
+
if (
|
|
117
|
+
!Number.isInteger(maxQueued) ||
|
|
118
|
+
maxQueued < 0 ||
|
|
119
|
+
maxQueued > MAX_RUNTIME_SCHEDULER_SUBMIT_ADMISSION_QUEUED
|
|
120
|
+
) {
|
|
121
|
+
throw new Error('Runtime scheduler submit admission queue is invalid.');
|
|
122
|
+
}
|
|
123
|
+
return maxQueued;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function drainEntry(key: string, entry: AdmissionEntry): void {
|
|
127
|
+
while (entry.active < entry.maxActive && entry.waiters.length > 0) {
|
|
128
|
+
const next = entry.waiters.shift()!;
|
|
129
|
+
clearTimeout(next.timeout);
|
|
130
|
+
entry.active += 1;
|
|
131
|
+
next.resolve(permitFor(key, entry, entry.waiters.length, next.enqueuedAt));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
82
135
|
function permitFor(
|
|
83
136
|
key: string,
|
|
84
137
|
entry: AdmissionEntry,
|
|
@@ -92,15 +145,8 @@ function permitFor(
|
|
|
92
145
|
release: () => {
|
|
93
146
|
if (released) return;
|
|
94
147
|
released = true;
|
|
95
|
-
const next = entry.waiters.shift();
|
|
96
|
-
if (next) {
|
|
97
|
-
clearTimeout(next.timeout);
|
|
98
|
-
next.resolve(
|
|
99
|
-
permitFor(key, entry, entry.waiters.length, next.enqueuedAt),
|
|
100
|
-
);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
148
|
entry.active -= 1;
|
|
149
|
+
drainEntry(key, entry);
|
|
104
150
|
pruneEntry(key, entry);
|
|
105
151
|
},
|
|
106
152
|
};
|
|
@@ -116,16 +162,34 @@ export async function acquireRuntimeSchedulerSubmitAdmission(
|
|
|
116
162
|
): Promise<RuntimeSchedulerSubmitAdmissionPermit> {
|
|
117
163
|
const key = keyFor(params);
|
|
118
164
|
const entries = registry();
|
|
119
|
-
const
|
|
165
|
+
const requestedMaxActive = maxActiveFor(params);
|
|
166
|
+
const requestedMaxQueued = maxQueuedFor(params);
|
|
167
|
+
const entry = entries.get(key) ?? {
|
|
168
|
+
active: 0,
|
|
169
|
+
maxActive: requestedMaxActive,
|
|
170
|
+
maxQueued: requestedMaxQueued,
|
|
171
|
+
waiters: [],
|
|
172
|
+
};
|
|
120
173
|
entries.set(key, entry);
|
|
121
174
|
const enqueuedAt = Date.now();
|
|
122
175
|
|
|
123
|
-
|
|
176
|
+
// A private batch may arrive after ordinary work has established this lane.
|
|
177
|
+
// Upgrade its shared gate in place so both call paths stay under one actual
|
|
178
|
+
// pool ceiling instead of creating independent per-protocol queues.
|
|
179
|
+
if (requestedMaxActive > entry.maxActive) {
|
|
180
|
+
entry.maxActive = requestedMaxActive;
|
|
181
|
+
drainEntry(key, entry);
|
|
182
|
+
}
|
|
183
|
+
if (requestedMaxQueued > entry.maxQueued) {
|
|
184
|
+
entry.maxQueued = requestedMaxQueued;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (entry.active < entry.maxActive && entry.waiters.length === 0) {
|
|
124
188
|
entry.active += 1;
|
|
125
189
|
return permitFor(key, entry, 0, enqueuedAt);
|
|
126
190
|
}
|
|
127
191
|
|
|
128
|
-
if (entry.waiters.length >=
|
|
192
|
+
if (entry.waiters.length >= entry.maxQueued) {
|
|
129
193
|
pruneEntry(key, entry);
|
|
130
194
|
throw new RuntimeSchedulerSubmissionAdmissionError(
|
|
131
195
|
'full',
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
classifyPlayAdmissionFailure,
|
|
4
|
+
PLAY_RUN_PRE_RUN_VALIDATION_FAILED,
|
|
5
|
+
RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE,
|
|
6
|
+
} from './/play-admission-outcome';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
PLAY_RUN_PRE_RUN_VALIDATION_FAILED,
|
|
10
|
+
RUNTIME_DB_SESSION_PRELOAD_INCOMPLETE,
|
|
11
|
+
};
|
|
6
12
|
|
|
7
13
|
const RETRY_BASE_MS = 1_000;
|
|
8
14
|
const RETRY_CAP_MS = 15 * 60 * 1_000;
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* Compatibility facade for the old webhook-named import. New source adapters
|
|
18
|
+
* use `classifyPlayAdmissionFailure`, while an older Vercel build can keep its
|
|
19
|
+
* boolean result shape during the Convex-before-Vercel rollout window.
|
|
14
20
|
*/
|
|
15
|
-
export function classifyWebhookReceiptAdmissionFailure(error: unknown)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return { retryable: false, code: PLAY_SECRET_UNAVAILABLE };
|
|
22
|
-
}
|
|
23
|
-
return { retryable: true, code: 'INTERNAL_ADMISSION_FAILURE' };
|
|
21
|
+
export function classifyWebhookReceiptAdmissionFailure(error: unknown) {
|
|
22
|
+
const failure = classifyPlayAdmissionFailure(error);
|
|
23
|
+
return {
|
|
24
|
+
retryable: failure.disposition === 'retry',
|
|
25
|
+
code: failure.code,
|
|
26
|
+
};
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -40,11 +43,8 @@ export function webhookReceiptRetryDelayMs(input: {
|
|
|
40
43
|
.update(String(input.attempt))
|
|
41
44
|
.digest();
|
|
42
45
|
// Keep a nonzero delay and give concurrent deliveries independent stable
|
|
43
|
-
// placement across the exponential window.
|
|
44
|
-
//
|
|
46
|
+
// placement across the exponential window. Do not multiply a capped ceiling:
|
|
47
|
+
// that would pin every long-lived retry to the same 15-minute boundary.
|
|
45
48
|
const fraction = hash.readUInt32BE(0) / 0xffff_ffff;
|
|
46
|
-
return Math.
|
|
47
|
-
RETRY_CAP_MS,
|
|
48
|
-
Math.max(RETRY_BASE_MS, Math.round(ceiling * (1 + fraction))),
|
|
49
|
-
);
|
|
49
|
+
return Math.round(RETRY_BASE_MS + (ceiling - RETRY_BASE_MS) * fraction);
|
|
50
50
|
}
|
|
@@ -1572,7 +1572,10 @@ function docflowObservedExpression(input: {
|
|
|
1572
1572
|
? `async()=>(${input.expression})`
|
|
1573
1573
|
: `()=>(${input.expression})`;
|
|
1574
1574
|
const observation = `((runtimeContext,inputCaptures,execute)=>typeof runtimeContext.__deeplineObserveDocflowNode==="function"?runtimeContext.__deeplineObserveDocflowNode(${JSON.stringify(input.nodeId)},inputCaptures,${JSON.stringify(input.outputs)},execute):execute())(${input.contextName},${docflowInputCaptureSource(input.inputs)},${callback})`;
|
|
1575
|
-
|
|
1575
|
+
// Parenthesize the awaited form so it is self-delimiting at every source
|
|
1576
|
+
// boundary, including compact `return{...}` syntax. This avoids relying on
|
|
1577
|
+
// whitespace repair around a character-offset replacement.
|
|
1578
|
+
return input.awaitsResult ? `(await ${observation})` : observation;
|
|
1576
1579
|
}
|
|
1577
1580
|
|
|
1578
1581
|
function expressionContainsTopLevelAwait(expression: AstNode): boolean {
|
|
@@ -425,6 +425,70 @@ const PRODUCT_NOTIFICATION_TYPE_REGISTRY = [
|
|
|
425
425
|
},
|
|
426
426
|
},
|
|
427
427
|
},
|
|
428
|
+
{
|
|
429
|
+
id: 'billing.auto_topup.limit_reached',
|
|
430
|
+
eventName: ProductNotificationEvent.AutoTopupLimitObserved,
|
|
431
|
+
requiredFilter: {},
|
|
432
|
+
allowedTriggers: ['state_transition'],
|
|
433
|
+
catalog: {
|
|
434
|
+
name: 'Auto top-up purchase limit reached',
|
|
435
|
+
category: 'credits',
|
|
436
|
+
description:
|
|
437
|
+
'Notify when the rolling 30-day purchase cap prevents an automatic top-up.',
|
|
438
|
+
allowMultiple: false,
|
|
439
|
+
editor: { kind: 'none' },
|
|
440
|
+
},
|
|
441
|
+
rule: {
|
|
442
|
+
default: {
|
|
443
|
+
ruleKey: 'default.auto_topup_limit_reached',
|
|
444
|
+
groupKey: 'default',
|
|
445
|
+
},
|
|
446
|
+
conditionSubject: 'workspace',
|
|
447
|
+
create: (_input, base) => ({
|
|
448
|
+
...base,
|
|
449
|
+
trigger: { kind: 'state_transition', targetState: 'blocked' },
|
|
450
|
+
}),
|
|
451
|
+
validate: (trigger) => {
|
|
452
|
+
if (
|
|
453
|
+
trigger.kind !== 'state_transition' ||
|
|
454
|
+
trigger.targetState !== 'blocked' ||
|
|
455
|
+
trigger.reasonCode !== undefined
|
|
456
|
+
) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
'Auto-top-up limit rules must enter the blocked state.',
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
present: ({ payload }) => ({
|
|
463
|
+
title: 'Auto top-up purchase limit reached',
|
|
464
|
+
summary: [
|
|
465
|
+
'An automatic top-up for ',
|
|
466
|
+
{ workspace: true },
|
|
467
|
+
' was blocked by its rolling 30-day purchase cap.',
|
|
468
|
+
],
|
|
469
|
+
details: [
|
|
470
|
+
[
|
|
471
|
+
'Purchase cap: ',
|
|
472
|
+
{
|
|
473
|
+
strong: `${formatCredits(Number(payload.purchaseLimitCredits))} credits`,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
[
|
|
477
|
+
'Remaining purchase allowance: ',
|
|
478
|
+
{
|
|
479
|
+
strong: `${formatCredits(Number(payload.remainingCredits))} credits`,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
'The remaining allowance cannot cover the minimum top-up. Pending automatic purchases also reserve allowance.',
|
|
483
|
+
'You can continue using your remaining credits, subject to your usage cap. Increase or remove the purchase cap in Billing, or wait for earlier purchases to leave the rolling 30-day window.',
|
|
484
|
+
],
|
|
485
|
+
targetUrl: String(payload.targetUrl),
|
|
486
|
+
subjectKind: 'workspace',
|
|
487
|
+
subjectId: String(payload.accountId),
|
|
488
|
+
severity: 'warning',
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
},
|
|
428
492
|
{
|
|
429
493
|
id: 'credits.low',
|
|
430
494
|
eventName: ProductNotificationEvent.CreditsBalanceChanged,
|
|
@@ -17,6 +17,7 @@ export const ProductNotificationEvent = {
|
|
|
17
17
|
CreditsBalanceChanged: 'credits.balance.changed',
|
|
18
18
|
CreditsPurchaseCompleted: 'credits.purchase.completed',
|
|
19
19
|
CreditsUsageRecorded: 'credits.usage.recorded',
|
|
20
|
+
AutoTopupLimitObserved: 'billing.auto_topup.limit_observed',
|
|
20
21
|
BillingPlanPurchased: 'billing.plan.purchased',
|
|
21
22
|
MonitorLifecycleChanged: 'monitor.lifecycle.changed',
|
|
22
23
|
PlayLifecycleChanged: 'play.lifecycle.changed',
|
|
@@ -56,6 +57,7 @@ export const PRODUCT_NOTIFICATION_EVENT_NAMES = [
|
|
|
56
57
|
ProductNotificationEvent.PlayRunCompleted,
|
|
57
58
|
ProductNotificationEvent.WebhookDeliveryCompleted,
|
|
58
59
|
ProductNotificationEvent.CreditsUsageRecorded,
|
|
60
|
+
ProductNotificationEvent.AutoTopupLimitObserved,
|
|
59
61
|
ProductNotificationEvent.CreditsPurchaseCompleted,
|
|
60
62
|
ProductNotificationEvent.BillingPlanPurchased,
|
|
61
63
|
] as const;
|
|
@@ -180,6 +182,20 @@ export const PRODUCT_NOTIFICATION_EVENT_DEFINITIONS: readonly ProductNotificatio
|
|
|
180
182
|
allowedTriggers: ['threshold_crossing'],
|
|
181
183
|
allowedThresholdUnits: ['percent', 'usage'],
|
|
182
184
|
},
|
|
185
|
+
{
|
|
186
|
+
eventName: ProductNotificationEvent.AutoTopupLimitObserved,
|
|
187
|
+
producerKey: ProductNotificationProducerKey.BillingCredits,
|
|
188
|
+
requiredFacts: {
|
|
189
|
+
accountId: 'string',
|
|
190
|
+
currentState: 'string',
|
|
191
|
+
purchaseLimitCredits: 'number',
|
|
192
|
+
committedCredits: 'number',
|
|
193
|
+
remainingCredits: 'number',
|
|
194
|
+
targetUrl: 'string',
|
|
195
|
+
},
|
|
196
|
+
orderingSubjectFact: 'accountId',
|
|
197
|
+
allowedTriggers: ['state_transition'],
|
|
198
|
+
},
|
|
183
199
|
{
|
|
184
200
|
// One paid credit purchase (manual or automatic recharge) settled.
|
|
185
201
|
eventName: ProductNotificationEvent.CreditsPurchaseCompleted,
|
|
@@ -389,7 +405,10 @@ export function adaptProductNotificationObservation(input: {
|
|
|
389
405
|
if (trigger.kind === 'state_transition') {
|
|
390
406
|
return {
|
|
391
407
|
kind: 'state_transition',
|
|
392
|
-
previousState:
|
|
408
|
+
previousState:
|
|
409
|
+
typeof event.payload.previousState === 'string'
|
|
410
|
+
? event.payload.previousState
|
|
411
|
+
: undefined,
|
|
393
412
|
currentState: String(event.payload.currentState),
|
|
394
413
|
previousReasonCode:
|
|
395
414
|
typeof event.payload.previousReasonCode === 'string'
|