@cosmicdrift/kumiko-samples 0.338.0 → 0.340.0
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/package.json +1 -1
- package/packages/bundled-features/package.json +21 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +7 -0
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +2 -6
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +3 -5
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +2 -8
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +35 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +60 -0
- package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/index.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +15 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -7
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -5
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -7
- package/packages/bundled-features/src/billing-foundation/changes.json +41 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +11 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-text.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +48 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +10 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +32 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +129 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +13 -59
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +20 -21
- package/packages/bundled-features/src/billing-foundation/events.ts +18 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +4 -5
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +6 -5
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/projection.ts +35 -41
- package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +4 -4
- package/packages/bundled-features/src/billing-foundation/types.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +6 -0
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +32 -0
- package/packages/bundled-features/src/channel-discord/feature.ts +40 -0
- package/packages/bundled-features/src/channel-discord/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +38 -0
- package/packages/bundled-features/src/channel-slack/index.ts +2 -0
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +29 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +42 -0
- package/packages/bundled-features/src/channel-teams/index.ts +2 -0
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +50 -0
- package/packages/bundled-features/src/channel-telegram/feature.ts +35 -0
- package/packages/bundled-features/src/channel-telegram/index.ts +2 -0
- package/packages/bundled-features/src/channel-telegram/telegram-channel.ts +74 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +6 -18
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/handlers/define-tenant-field.write.ts +1 -4
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +1 -1
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +1 -1
- package/packages/bundled-features/src/data-retention/feature.ts +2 -3
- package/packages/bundled-features/src/delivery/changes.json +7 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +3 -1
- package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +76 -0
- package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +135 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +3 -0
- package/packages/bundled-features/src/delivery/index.ts +15 -0
- package/packages/bundled-features/src/delivery/jobs.ts +1 -1
- package/packages/bundled-features/src/delivery/testing.ts +2 -0
- package/packages/bundled-features/src/delivery/types.ts +8 -3
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/handlers/save.write.ts +1 -5
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +14 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +29 -13
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +201 -0
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +57 -58
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +17 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +74 -36
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +7 -10
- package/packages/bundled-features/src/renderer-simple/changes.json +9 -1
- package/packages/bundled-features/src/renderer-simple/feature.ts +14 -4
- package/packages/bundled-features/src/renderer-simple/index.ts +2 -2
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +94 -31
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/handlers/claim.write.ts +1 -4
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -3
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +3 -5
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +1 -1
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -1
- package/packages/bundled-features/src/user/changes.json +13 -6
- package/packages/bundled-features/src/user/handlers/update-roles.ts +7 -23
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -6
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +1 -3
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/lift-restriction.write.ts +2 -6
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion-by-email.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/restrict-account.write.ts +2 -8
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +14 -0
- package/packages/bundled-features/src/workflow-runner/db/queries/due-runs.ts +10 -13
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +7 -2
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +5 -0
- package/packages/bundled-features/src/workflow-runner/feature.ts +4 -11
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +231 -77
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -0
- package/packages/bundled-features/src/workflow-runner/runner.ts +12 -2
- package/packages/bundled-features/src/workflow-runner/tables.ts +8 -1
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +5 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +11 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +16 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +152 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +82 -0
- package/samples/apps/use-all-bundled/feature-manifest.json +146 -1
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-gallery.config.ts +14 -0
- package/samples/apps/use-all-bundled/schema/generate.ts +20 -0
- package/samples/apps/use-all-bundled/scripts/gen-feature-manifest.ts +1 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +10 -0
- package/samples/apps/use-all-bundled/tsconfig.json +1 -0
- package/samples/recipes/chat-channels/README.md +19 -0
- package/samples/recipes/chat-channels/package.json +9 -0
- package/samples/recipes/chat-channels/src/feature.ts +40 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/workflow-engine/src/event-trigger.ts +5 -0
- package/samples/recipes/workflow-engine/src/postgres-resume-loop.ts +50 -9
- package/samples/recipes/workflow-engine/src/workflow-runner.ts +12 -2
- package/packages/bundled-features/src/billing-foundation/db/queries/payment-projection.ts +0 -19
- package/packages/bundled-features/src/billing-foundation/db/queries/subscription-projection.ts +0 -15
- package/packages/bundled-features/src/inbound-mail-foundation/db/queries/inbound-projections.ts +0 -38
|
@@ -29,7 +29,7 @@ export type UpdateUserLifecycleOptions = {
|
|
|
29
29
|
readonly expect?: Readonly<Record<string, string | number | boolean | null>>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// `conn` is ctx.db.unsafeRaw(
|
|
32
|
+
// `conn` is ctx.db.unsafeRaw() (regular handlers) or the open tx
|
|
33
33
|
// (forget-cleanup sub-tx) — keeps the event append atomic with the write.
|
|
34
34
|
//
|
|
35
35
|
// Returns `{ applied: false }` instead of throwing only when `options.expect`
|
|
@@ -155,7 +155,7 @@ export interface RunForgetCleanupArgs {
|
|
|
155
155
|
|
|
156
156
|
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
157
157
|
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
158
|
-
// escapeHatch when its delete hook calls ctx.db.unsafeRaw(
|
|
158
|
+
// escapeHatch when its delete hook calls ctx.db.unsafeRaw().
|
|
159
159
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
160
160
|
readonly actor?: string;
|
|
161
161
|
}
|
|
@@ -60,7 +60,7 @@ export interface RunUserExportArgs {
|
|
|
60
60
|
readonly now: Instant;
|
|
61
61
|
// fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
|
|
62
62
|
// systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
|
|
63
|
-
// escapeHatch when its export hook calls ctx.db.unsafeRaw(
|
|
63
|
+
// escapeHatch when its export hook calls ctx.db.unsafeRaw().
|
|
64
64
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
65
65
|
readonly actor?: string;
|
|
66
66
|
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.340.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "The resume job reads due runs through the job's TenantDb",
|
|
6
|
+
"detail": "`selectDueWorkflowRunPending` takes a `TenantDb` and returns `{ runId, stepIndex }` rows, so the tenant filter comes from the TenantDb instead of a hand-written `WHERE tenant_id`.",
|
|
7
|
+
"migration": "keine"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.339.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "workflow-run stream stores a reference to the trigger event instead of its payload",
|
|
13
|
+
"detail": "`workflow.run-started` now holds `triggerEventRef: { eventId, aggregateId, version }` and no `triggerPayload`; the wait, waitForEvent and retry step events no longer embed it either. `workflow_run_pending` gets a `trigger_event_ref` column and the event-subscriber stops writing `trigger_payload`. `resume-run` loads the trigger and the awaited event through `ctx.loadAggregate` and fails the run with `trigger_event_unavailable` or `awaited_event_unavailable` when one is gone.",
|
|
14
|
+
"migration": "Own code that reads `triggerPayload` from `workflow.run-started` must read `triggerEventRef` and load the event with `ctx.loadAggregate(ref.aggregateId)`. Apps generate the migration for the new `workflow_run_pending.trigger_event_ref` column with `kumiko schema generate`. The step events `workflow.step.waiting`, `workflow.step.waiting-for-event` and `workflow.retry.scheduled` no longer carry `triggerPayload`; a custom resume loop that read it must load the trigger event through the `triggerEventRef` on its own run-started event (see `samples/recipes/workflow-engine/src/postgres-resume-loop.ts`). Stored run-started events and pending rows keep their old copy; this release rewrites no events, and runs started before it resume from that copy."
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "0.337.0",
|
|
4
18
|
"type": "fix",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { sql, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { workflowRunPendingTable } from "../../tables.js";
|
|
4
3
|
|
|
5
|
-
export type DueRow = { readonly
|
|
4
|
+
export type DueRow = { readonly runId: string; readonly stepIndex: number };
|
|
6
5
|
|
|
7
6
|
// Oldest first and capped, so a backlog after an outage drains over several
|
|
8
7
|
// ticks instead of holding one tick's `concurrency: "skip"` slot for minutes.
|
|
@@ -12,13 +11,11 @@ export const DUE_RUNS_BATCH_SIZE = 100;
|
|
|
12
11
|
// (ctx.tryAppendEvent on WORKFLOW_RESUMED) — a second dispatch for the same
|
|
13
12
|
// row loses that race and no-ops. No row locks: in an autocommit statement
|
|
14
13
|
// they would be released right after the SELECT and guard nothing.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
[tenantId],
|
|
23
|
-
)) as readonly DueRow[];
|
|
14
|
+
// `now()` stays the DB clock so job-host clock skew cannot mark rows due early.
|
|
15
|
+
export function selectDueWorkflowRunPending(db: TenantDb): Promise<readonly DueRow[]> {
|
|
16
|
+
return db.selectMany<DueRow>(
|
|
17
|
+
workflowRunPendingTable,
|
|
18
|
+
{ wakeAt: { lt: sql`now()` } },
|
|
19
|
+
{ orderBy: { col: "wakeAt", direction: "asc" }, limit: DUE_RUNS_BATCH_SIZE },
|
|
20
|
+
);
|
|
24
21
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// This subscriber never resumes a run itself (D3, workflow-resume-loop.md):
|
|
21
21
|
// the MultiStreamApplyContext it runs in has no callFeature/runStepList
|
|
22
22
|
// access, only unsafeAppendEvent/loadAggregate. It writes triggerEventType
|
|
23
|
-
// +
|
|
23
|
+
// + triggerEventRef + wakeAt=now() onto the matching pending row(s); the
|
|
24
24
|
// existing resume-due-runs job (Phase 2) picks the row up on its next
|
|
25
25
|
// cron tick — the one resume path stays exactly the one Phase 2 built.
|
|
26
26
|
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
import { evaluateEventMatch } from "@cosmicdrift/kumiko-framework/engine";
|
|
35
35
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
36
36
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
37
|
+
import type { TriggerEventRef } from "./runner.js";
|
|
37
38
|
import { workflowRunPendingTable } from "./tables.js";
|
|
38
39
|
|
|
39
40
|
type CandidateRow = {
|
|
@@ -87,7 +88,11 @@ const wakeupApply: MultiStreamApplyFn = async (event, tx) => {
|
|
|
87
88
|
workflowRunPendingTable,
|
|
88
89
|
{
|
|
89
90
|
triggerEventType: event.type,
|
|
90
|
-
|
|
91
|
+
triggerEventRef: {
|
|
92
|
+
eventId: event.id,
|
|
93
|
+
aggregateId: event.aggregateId,
|
|
94
|
+
version: event.version,
|
|
95
|
+
} satisfies TriggerEventRef,
|
|
91
96
|
wakeAt: getTemporal().Now.instant().toString(),
|
|
92
97
|
},
|
|
93
98
|
{ tenantId: row.tenantId, runId: row.runId, stepIndex: row.stepIndex },
|
|
@@ -85,6 +85,11 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
|
|
|
85
85
|
runId,
|
|
86
86
|
workflow,
|
|
87
87
|
triggerEvent,
|
|
88
|
+
triggerEventRef: {
|
|
89
|
+
eventId: event.id,
|
|
90
|
+
aggregateId: event.aggregateId,
|
|
91
|
+
version: event.version,
|
|
92
|
+
},
|
|
88
93
|
...(idempotencyKey && { idempotencyKey }),
|
|
89
94
|
// @cast-boundary msp-to-handler-ctx — MultiStreamApplyContext only
|
|
90
95
|
// exposes unsafeAppendEvent/loadAggregate, a subset of
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
//
|
|
25
25
|
// wait/retry/waitForEvent suspensions all resume automatically now.
|
|
26
26
|
|
|
27
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
28
27
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
29
28
|
import { selectDueWorkflowRunPending } from "./db/queries/due-runs.js";
|
|
30
29
|
import { resumeRunHandler } from "./handlers/resume-run.write.js";
|
|
@@ -68,26 +67,20 @@ export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
68
67
|
return;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
ctx.systemDb.assertTenantMatch(tenantId);
|
|
72
|
-
const dueRows = await selectDueWorkflowRunPending(
|
|
73
|
-
ctx.systemDb.unsafeRaw(
|
|
74
|
-
"due-row pickup needs FOR UPDATE SKIP LOCKED raw SQL; the query filters by this job's tenantId",
|
|
75
|
-
) as DbConnection, // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
|
|
76
|
-
tenantId,
|
|
77
|
-
);
|
|
70
|
+
const dueRows = await selectDueWorkflowRunPending(ctx.systemDb.assertTenantMatch(tenantId));
|
|
78
71
|
|
|
79
72
|
for (const row of dueRows) {
|
|
80
73
|
try {
|
|
81
74
|
await ctx.write("workflow-runner:write:resume-run", {
|
|
82
|
-
runId: row.
|
|
83
|
-
stepIndex: row.
|
|
75
|
+
runId: row.runId,
|
|
76
|
+
stepIndex: row.stepIndex,
|
|
84
77
|
});
|
|
85
78
|
} catch (error) {
|
|
86
79
|
// skip: one row's dispatch failing (infra hiccup, not a business
|
|
87
80
|
// outcome — resume-run itself never throws for expected cases)
|
|
88
81
|
// must not stop the rest of this tenant's due rows from resuming.
|
|
89
82
|
ctx.log.warn(
|
|
90
|
-
`[workflow-runner:resume-due-runs] runId=${row.
|
|
83
|
+
`[workflow-runner:resume-due-runs] runId=${row.runId} stepIndex=${row.stepIndex} dispatch failed: ${String(error)}`,
|
|
91
84
|
);
|
|
92
85
|
}
|
|
93
86
|
}
|
|
@@ -16,16 +16,19 @@
|
|
|
16
16
|
// otherwise (wait already wrote its effect; resume past it).
|
|
17
17
|
//
|
|
18
18
|
// The run's ORIGINAL trigger event (the one that started the whole run) is
|
|
19
|
-
// always
|
|
20
|
-
// never from the pending row
|
|
21
|
-
// WorkflowRunStartedPayload in ./runner).
|
|
22
|
-
//
|
|
23
|
-
//
|
|
19
|
+
// always re-read from the event store via the triggerEventRef on the run's
|
|
20
|
+
// own WORKFLOW_RUN_STARTED_TYPE event, never from the pending row (see
|
|
21
|
+
// WorkflowRunStartedPayload in ./runner). Neither the run stream nor the
|
|
22
|
+
// pending row holds a copy of a foreign payload, so an erased source event
|
|
23
|
+
// shows up here in its shredded form, and an unloadable one (archived
|
|
24
|
+
// stream) fails the run. The pending row's own
|
|
25
|
+
// triggerEventType/triggerEventRef columns are a DIFFERENT thing: for a
|
|
26
|
+
// waitForEvent suspension they point at the AWAITED event the Phase 3b
|
|
24
27
|
// event-subscriber matched (NULL until then, and NULL forever on a
|
|
25
|
-
// timeout-without-a-match). That payload becomes the resumed
|
|
26
|
-
// result for the skipped waitForEvent step itself (see
|
|
27
|
-
// steps/wait-for-event.ts) — pre-seeded into stepsAcc below so
|
|
28
|
-
// subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
|
|
28
|
+
// timeout-without-a-match). That event's payload becomes the resumed
|
|
29
|
+
// pipeline's result for the skipped waitForEvent step itself (see
|
|
30
|
+
// resultKey on steps/wait-for-event.ts) — pre-seeded into stepsAcc below so
|
|
31
|
+
// a subsequent step's resolver can read `ctx.steps[awaits.someKey]`.
|
|
29
32
|
|
|
30
33
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
31
34
|
import {
|
|
@@ -53,9 +56,9 @@ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
|
53
56
|
import * as z from "zod";
|
|
54
57
|
import {
|
|
55
58
|
isResumableSuspension,
|
|
59
|
+
type TriggerEventRef,
|
|
56
60
|
type WorkflowRunCompletedPayload,
|
|
57
61
|
type WorkflowRunFailedPayload,
|
|
58
|
-
type WorkflowRunStartedPayload,
|
|
59
62
|
WorkflowSuspensionUnsupportedError,
|
|
60
63
|
} from "../runner.js";
|
|
61
64
|
import { workflowRunPendingTable } from "../tables.js";
|
|
@@ -182,50 +185,218 @@ function isRunAlreadySettled(
|
|
|
182
185
|
});
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
type LoadedEvents = Awaited<ReturnType<HandlerContext["loadAggregate"]>>;
|
|
189
|
+
|
|
190
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
191
|
+
return typeof value === "object" && value !== null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isTriggerEventRef(value: unknown): value is TriggerEventRef {
|
|
195
|
+
return (
|
|
196
|
+
isRecord(value) &&
|
|
197
|
+
typeof value["eventId"] === "string" &&
|
|
198
|
+
typeof value["aggregateId"] === "string" &&
|
|
199
|
+
typeof value["version"] === "number"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// run-started events written before the reference existed carry a copy of the
|
|
204
|
+
// trigger payload instead; they stay readable for runs still in flight.
|
|
205
|
+
type TriggerSource =
|
|
206
|
+
| { readonly kind: "ref"; readonly eventType: string; readonly ref: TriggerEventRef }
|
|
207
|
+
| { readonly kind: "legacy-copy"; readonly eventType: string; readonly payload: unknown };
|
|
208
|
+
|
|
209
|
+
function readTriggerSource(startedPayload: unknown): TriggerSource | null {
|
|
210
|
+
if (!isRecord(startedPayload) || typeof startedPayload["triggerEventType"] !== "string") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const eventType = startedPayload["triggerEventType"];
|
|
214
|
+
const ref = startedPayload["triggerEventRef"];
|
|
215
|
+
if (isTriggerEventRef(ref)) {
|
|
216
|
+
return { kind: "ref", eventType, ref };
|
|
217
|
+
}
|
|
218
|
+
if ("triggerPayload" in startedPayload) {
|
|
219
|
+
return { kind: "legacy-copy", eventType, payload: startedPayload["triggerPayload"] };
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function loadReferencedEvent(ctx: HandlerContext, ref: TriggerEventRef) {
|
|
225
|
+
const events = await ctx.loadAggregate(ref.aggregateId);
|
|
226
|
+
return events.find((e) => e.id === ref.eventId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
type RecoveredTrigger =
|
|
230
|
+
| { readonly event: WriteEvent; readonly unavailable: false }
|
|
231
|
+
| { readonly event: null; readonly unavailable: true };
|
|
232
|
+
|
|
233
|
+
async function recoverTriggerEvent(
|
|
234
|
+
ctx: HandlerContext,
|
|
235
|
+
runEvents: LoadedEvents,
|
|
187
236
|
runId: string,
|
|
188
237
|
user: WriteEvent["user"],
|
|
189
|
-
):
|
|
238
|
+
): Promise<RecoveredTrigger> {
|
|
190
239
|
const started = runEvents.find((e) => e.type === WORKFLOW_RUN_STARTED_TYPE);
|
|
191
240
|
if (!started) {
|
|
192
241
|
throw new InternalError({
|
|
193
242
|
message: `workflow-runner:write:resume-run: run ${runId} has no ${WORKFLOW_RUN_STARTED_TYPE} event — cannot recover its trigger event.`,
|
|
194
243
|
});
|
|
195
244
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
245
|
+
const source = readTriggerSource(started.payload);
|
|
246
|
+
if (!source) {
|
|
247
|
+
throw new InternalError({
|
|
248
|
+
message: `workflow-runner:write:resume-run: run ${runId} has a malformed ${WORKFLOW_RUN_STARTED_TYPE} event — neither triggerEventRef nor a legacy triggerPayload.`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (source.kind === "legacy-copy") {
|
|
252
|
+
return {
|
|
253
|
+
event: { type: source.eventType, payload: source.payload, user },
|
|
254
|
+
unavailable: false,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const triggerEvent = await loadReferencedEvent(ctx, source.ref);
|
|
258
|
+
if (!triggerEvent) {
|
|
259
|
+
return { event: null, unavailable: true };
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
event: { type: source.eventType, payload: triggerEvent.payload, user },
|
|
263
|
+
unavailable: false,
|
|
201
264
|
};
|
|
202
|
-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type PendingTriggerColumns = {
|
|
268
|
+
readonly triggerEventRef: unknown | null;
|
|
269
|
+
readonly triggerPayload: unknown | null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
type AwaitedPayload =
|
|
273
|
+
| { readonly unavailable: false; readonly payload: unknown }
|
|
274
|
+
| { readonly unavailable: true };
|
|
275
|
+
|
|
276
|
+
// NULL ref + NULL payload = timeout without a match: the step result stays
|
|
277
|
+
// undefined, same as before. A matched row whose event can no longer be loaded
|
|
278
|
+
// is NOT folded into that case: a later step could not tell "nothing arrived"
|
|
279
|
+
// from "arrived but gone" and would run on missing data, so the run fails
|
|
280
|
+
// instead. Rows matched before the reference existed still carry the copy.
|
|
281
|
+
async function loadAwaitedPayload(
|
|
282
|
+
ctx: HandlerContext,
|
|
283
|
+
pending: PendingTriggerColumns,
|
|
284
|
+
): Promise<AwaitedPayload> {
|
|
285
|
+
if (!isTriggerEventRef(pending.triggerEventRef)) {
|
|
286
|
+
return { unavailable: false, payload: pending.triggerPayload ?? undefined };
|
|
287
|
+
}
|
|
288
|
+
const awaited = await loadReferencedEvent(ctx, pending.triggerEventRef);
|
|
289
|
+
return awaited ? { unavailable: false, payload: awaited.payload } : { unavailable: true };
|
|
203
290
|
}
|
|
204
291
|
|
|
205
292
|
// The suspended waitForEvent step itself is skipped on resume
|
|
206
293
|
// (resumeFrom = stepIndex + 1) — its run() never re-executes, so its
|
|
207
294
|
// resultKey (args.event, see steps/wait-for-event.ts) never gets
|
|
208
|
-
// populated by the normal runStepList loop. Seed it here from the
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// other step result. NULL on a timeout-without-a-match — a later
|
|
212
|
-
// resolver reading it just sees `undefined`, no special-casing needed.
|
|
295
|
+
// populated by the normal runStepList loop. Seed it here from the awaited
|
|
296
|
+
// event's payload so a subsequent step's resolver sees the matched event via
|
|
297
|
+
// `ctx.steps[awaits.someKey]`, same as any other step result.
|
|
213
298
|
function seedResumedStepResults(
|
|
214
|
-
|
|
299
|
+
suspensionEventType: string,
|
|
300
|
+
awaitedPayload: unknown,
|
|
215
301
|
steps: readonly StepInstance[],
|
|
216
302
|
stepIndex: number,
|
|
217
303
|
): Record<string, unknown> {
|
|
218
304
|
const stepsAcc: Record<string, unknown> = {};
|
|
219
|
-
if (
|
|
305
|
+
if (suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE) {
|
|
220
306
|
const suspendedStep = steps[stepIndex];
|
|
221
307
|
const key = suspendedStep && getStep(suspendedStep.kind)?.resultKey?.(suspendedStep.args);
|
|
222
308
|
if (key !== undefined) {
|
|
223
|
-
stepsAcc[key] =
|
|
309
|
+
stepsAcc[key] = awaitedPayload;
|
|
224
310
|
}
|
|
225
311
|
}
|
|
226
312
|
return stepsAcc;
|
|
227
313
|
}
|
|
228
314
|
|
|
315
|
+
type ResumedPipelineInput = {
|
|
316
|
+
readonly workflow: WorkflowDefinition;
|
|
317
|
+
readonly workflowName: string;
|
|
318
|
+
readonly runId: string;
|
|
319
|
+
readonly stepIndex: number;
|
|
320
|
+
readonly resumeFrom: number;
|
|
321
|
+
readonly triggerEvent: WriteEvent;
|
|
322
|
+
readonly awaitedPayload: unknown;
|
|
323
|
+
readonly pending: {
|
|
324
|
+
readonly suspensionEventType: string;
|
|
325
|
+
readonly retryAttempt: number | null;
|
|
326
|
+
readonly definitionFingerprint: string | null;
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
async function runResumedPipeline(ctx: HandlerContext, input: ResumedPipelineInput) {
|
|
331
|
+
const {
|
|
332
|
+
workflow,
|
|
333
|
+
workflowName,
|
|
334
|
+
runId,
|
|
335
|
+
stepIndex,
|
|
336
|
+
resumeFrom,
|
|
337
|
+
triggerEvent,
|
|
338
|
+
awaitedPayload,
|
|
339
|
+
pending,
|
|
340
|
+
} = input;
|
|
341
|
+
try {
|
|
342
|
+
const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
|
|
343
|
+
const workflowCtx = {
|
|
344
|
+
runId,
|
|
345
|
+
workflowName,
|
|
346
|
+
stepIndex,
|
|
347
|
+
definitionFingerprint: pending.definitionFingerprint ?? undefined,
|
|
348
|
+
...(pending.retryAttempt !== null && { retryAttempt: pending.retryAttempt + 1 }),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const stepsAcc = seedResumedStepResults(
|
|
352
|
+
pending.suspensionEventType,
|
|
353
|
+
awaitedPayload,
|
|
354
|
+
steps,
|
|
355
|
+
stepIndex,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const outcome = await runStepList(
|
|
359
|
+
steps,
|
|
360
|
+
triggerEvent,
|
|
361
|
+
ctx,
|
|
362
|
+
stepsAcc,
|
|
363
|
+
{},
|
|
364
|
+
workflowCtx,
|
|
365
|
+
resumeFrom,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (outcome.kind === "suspended") {
|
|
369
|
+
if (!isResumableSuspension(steps, outcome.stepIndex)) {
|
|
370
|
+
throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
|
|
371
|
+
}
|
|
372
|
+
// Another suspension further down the pipeline — pending-projection.ts
|
|
373
|
+
// already materialised the new row; nothing more to do this pass.
|
|
374
|
+
return { isSuccess: true as const, data: { outcome: "suspended" as const } };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const completedPayload: WorkflowRunCompletedPayload = {
|
|
378
|
+
workflowName,
|
|
379
|
+
stepIndex: steps.length,
|
|
380
|
+
};
|
|
381
|
+
await ctx.unsafeAppendEvent({
|
|
382
|
+
aggregateId: runId,
|
|
383
|
+
aggregateType: WORKFLOW_AGGREGATE_TYPE,
|
|
384
|
+
type: WORKFLOW_RUN_COMPLETED_TYPE,
|
|
385
|
+
payload: completedPayload,
|
|
386
|
+
});
|
|
387
|
+
return { isSuccess: true as const, data: { outcome: "completed" as const } };
|
|
388
|
+
} catch (error) {
|
|
389
|
+
log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
|
|
390
|
+
const failedPayload: WorkflowRunFailedPayload = {
|
|
391
|
+
workflowName,
|
|
392
|
+
stepIndex,
|
|
393
|
+
error: describeWorkflowStepError(error),
|
|
394
|
+
};
|
|
395
|
+
await appendRunFailed(ctx, runId, failedPayload);
|
|
396
|
+
return { isSuccess: true as const, data: { outcome: "failed" as const } };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
229
400
|
export const resumeRunHandler: WriteHandlerDef = {
|
|
230
401
|
name: "resume-run",
|
|
231
402
|
agent: { expose: false },
|
|
@@ -248,6 +419,7 @@ export const resumeRunHandler: WriteHandlerDef = {
|
|
|
248
419
|
suspensionEventType: string;
|
|
249
420
|
retryAttempt: number | null;
|
|
250
421
|
definitionFingerprint: string | null;
|
|
422
|
+
triggerEventRef: unknown | null;
|
|
251
423
|
triggerPayload: unknown | null;
|
|
252
424
|
}>(db, workflowRunPendingTable, { runId, stepIndex, tenantId });
|
|
253
425
|
|
|
@@ -297,62 +469,44 @@ export const resumeRunHandler: WriteHandlerDef = {
|
|
|
297
469
|
return { isSuccess: true, data: { outcome: "already-resumed" as const } };
|
|
298
470
|
}
|
|
299
471
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
pending.suspensionEventType === WORKFLOW_RETRY_SCHEDULED_TYPE ? stepIndex : stepIndex + 1;
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const steps = buildPipelineSteps(workflow.pipelineDef, triggerEvent);
|
|
307
|
-
const workflowCtx = {
|
|
308
|
-
runId,
|
|
472
|
+
const recovered = await recoverTriggerEvent(ctx, runEvents, runId, event.user);
|
|
473
|
+
if (recovered.unavailable) {
|
|
474
|
+
await appendRunFailed(ctx, runId, {
|
|
309
475
|
workflowName,
|
|
310
476
|
stepIndex,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const stepsAcc = seedResumedStepResults(pending, steps, stepIndex);
|
|
316
|
-
|
|
317
|
-
const outcome = await runStepList(
|
|
318
|
-
steps,
|
|
319
|
-
triggerEvent,
|
|
320
|
-
ctx,
|
|
321
|
-
stepsAcc,
|
|
322
|
-
{},
|
|
323
|
-
workflowCtx,
|
|
324
|
-
resumeFrom,
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
if (outcome.kind === "suspended") {
|
|
328
|
-
if (!isResumableSuspension(steps, outcome.stepIndex)) {
|
|
329
|
-
throw new WorkflowSuspensionUnsupportedError(workflowName, outcome.stepIndex);
|
|
330
|
-
}
|
|
331
|
-
// Another suspension further down the pipeline — pending-projection.ts
|
|
332
|
-
// already materialised the new row; nothing more to do this pass.
|
|
333
|
-
return { isSuccess: true, data: { outcome: "suspended" as const } };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const completedPayload: WorkflowRunCompletedPayload = {
|
|
337
|
-
workflowName,
|
|
338
|
-
stepIndex: steps.length,
|
|
339
|
-
};
|
|
340
|
-
await ctx.unsafeAppendEvent({
|
|
341
|
-
aggregateId: runId,
|
|
342
|
-
aggregateType: WORKFLOW_AGGREGATE_TYPE,
|
|
343
|
-
type: WORKFLOW_RUN_COMPLETED_TYPE,
|
|
344
|
-
payload: completedPayload,
|
|
477
|
+
error: `Trigger event of run ${runId} is no longer available — cannot resume.`,
|
|
478
|
+
reason: "trigger_event_unavailable",
|
|
345
479
|
});
|
|
346
|
-
return { isSuccess: true, data: { outcome: "
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
480
|
+
return { isSuccess: true, data: { outcome: "failed" as const } };
|
|
481
|
+
}
|
|
482
|
+
const triggerEvent = recovered.event;
|
|
483
|
+
|
|
484
|
+
const awaited =
|
|
485
|
+
pending.suspensionEventType === WORKFLOW_WAITING_FOR_EVENT_TYPE
|
|
486
|
+
? await loadAwaitedPayload(ctx, pending)
|
|
487
|
+
: ({ unavailable: false, payload: undefined } as const);
|
|
488
|
+
if (awaited.unavailable) {
|
|
489
|
+
await appendRunFailed(ctx, runId, {
|
|
350
490
|
workflowName,
|
|
351
491
|
stepIndex,
|
|
352
|
-
error:
|
|
353
|
-
|
|
354
|
-
|
|
492
|
+
error: `Awaited event of run ${runId} is no longer available — cannot resume.`,
|
|
493
|
+
reason: "awaited_event_unavailable",
|
|
494
|
+
});
|
|
355
495
|
return { isSuccess: true, data: { outcome: "failed" as const } };
|
|
356
496
|
}
|
|
497
|
+
|
|
498
|
+
const resumeFrom =
|
|
499
|
+
pending.suspensionEventType === WORKFLOW_RETRY_SCHEDULED_TYPE ? stepIndex : stepIndex + 1;
|
|
500
|
+
|
|
501
|
+
return runResumedPipeline(ctx, {
|
|
502
|
+
workflow,
|
|
503
|
+
workflowName,
|
|
504
|
+
runId,
|
|
505
|
+
stepIndex,
|
|
506
|
+
resumeFrom,
|
|
507
|
+
triggerEvent,
|
|
508
|
+
awaitedPayload: awaited.payload,
|
|
509
|
+
pending,
|
|
510
|
+
});
|
|
357
511
|
},
|
|
358
512
|
};
|
|
@@ -84,6 +84,7 @@ async function upsertPending(tx: DbRunner, row: PendingRow): Promise<void> {
|
|
|
84
84
|
// Phase 3's event-subscriber writes these when the awaited event
|
|
85
85
|
// arrives — Phase 1 never touches them, insert or update.
|
|
86
86
|
triggerEventType: null,
|
|
87
|
+
triggerEventRef: null,
|
|
87
88
|
triggerPayload: null,
|
|
88
89
|
},
|
|
89
90
|
{ conflictKeys: ["tenantId", "runId", "stepIndex"] },
|
|
@@ -35,10 +35,19 @@ export function isResumableSuspension(steps: readonly StepInstance[], stepIndex:
|
|
|
35
35
|
return RESUMABLE_STEP_KINDS.has(steps[stepIndex]?.kind ?? "");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Points at the trigger event in the event store. The run stream never holds a
|
|
39
|
+
// copy of a foreign payload: erasing or archiving the source event must also
|
|
40
|
+
// cover the workflow, so a resume re-reads the event through this reference.
|
|
41
|
+
export type TriggerEventRef = {
|
|
42
|
+
readonly eventId: string;
|
|
43
|
+
readonly aggregateId: string;
|
|
44
|
+
readonly version: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
38
47
|
export type WorkflowRunStartedPayload = {
|
|
39
48
|
readonly workflowName: string;
|
|
40
49
|
readonly triggerEventType: string;
|
|
41
|
-
readonly
|
|
50
|
+
readonly triggerEventRef: TriggerEventRef;
|
|
42
51
|
readonly definitionFingerprint: string;
|
|
43
52
|
readonly idempotencyKey?: string;
|
|
44
53
|
};
|
|
@@ -91,6 +100,7 @@ export async function startAndRunWorkflow(args: {
|
|
|
91
100
|
readonly runId: string;
|
|
92
101
|
readonly workflow: WorkflowDefinition;
|
|
93
102
|
readonly triggerEvent: WriteEvent;
|
|
103
|
+
readonly triggerEventRef: TriggerEventRef;
|
|
94
104
|
readonly idempotencyKey?: string;
|
|
95
105
|
readonly handlerCtx: HandlerContext;
|
|
96
106
|
}): Promise<{ readonly outcome: "completed" | "suspended" }> {
|
|
@@ -99,7 +109,7 @@ export async function startAndRunWorkflow(args: {
|
|
|
99
109
|
const startedPayload: WorkflowRunStartedPayload = {
|
|
100
110
|
workflowName: args.workflow.name,
|
|
101
111
|
triggerEventType: args.triggerEvent.type,
|
|
102
|
-
|
|
112
|
+
triggerEventRef: args.triggerEventRef,
|
|
103
113
|
definitionFingerprint: fingerprint,
|
|
104
114
|
...(args.idempotencyKey && { idempotencyKey: args.idempotencyKey }),
|
|
105
115
|
};
|
|
@@ -52,8 +52,14 @@ export const workflowRunPendingTable = pgTable(
|
|
|
52
52
|
waitEventType: text("wait_event_type"),
|
|
53
53
|
matchExpr: jsonb("match_expr"),
|
|
54
54
|
// Written by the Phase 3 event-subscriber when the awaited event
|
|
55
|
-
// arrives — always NULL in Phase 1/2.
|
|
55
|
+
// arrives — always NULL in Phase 1/2. The row points at the awaited event
|
|
56
|
+
// ({ eventId, aggregateId, version }); resume-run re-reads it from the
|
|
57
|
+
// event store, so erasing the event also covers the workflow.
|
|
56
58
|
triggerEventType: text("trigger_event_type"),
|
|
59
|
+
triggerEventRef: jsonb("trigger_event_ref"),
|
|
60
|
+
// Legacy copy of the awaited payload. No longer written; only rows
|
|
61
|
+
// matched before the reference existed still carry it, and resume-run
|
|
62
|
+
// falls back to it for those.
|
|
57
63
|
triggerPayload: jsonb("trigger_payload"),
|
|
58
64
|
},
|
|
59
65
|
(t) => [
|
|
@@ -82,6 +88,7 @@ export const workflowRunPendingTableMeta: EntityTableMeta = defineUnmanagedTable
|
|
|
82
88
|
{ name: "wait_event_type", pgType: "text", notNull: false },
|
|
83
89
|
{ name: "match_expr", pgType: "jsonb", notNull: false },
|
|
84
90
|
{ name: "trigger_event_type", pgType: "text", notNull: false },
|
|
91
|
+
{ name: "trigger_event_ref", pgType: "jsonb", notNull: false },
|
|
85
92
|
{ name: "trigger_payload", pgType: "jsonb", notNull: false },
|
|
86
93
|
],
|
|
87
94
|
indexes: [
|
|
@@ -55,6 +55,11 @@ export const WIDGETS_I18N = {
|
|
|
55
55
|
de: "Antwortzeit-Verlauf",
|
|
56
56
|
en: "Response time history",
|
|
57
57
|
},
|
|
58
|
+
"widgets:catalog:response-time-p95": { de: "p95: 180 ms", en: "p95: 180 ms" },
|
|
59
|
+
"widgets:catalog:response-time-p95-aria": {
|
|
60
|
+
de: "p95-Schwelle der letzten 24 Stunden",
|
|
61
|
+
en: "p95 threshold over the last 24 hours",
|
|
62
|
+
},
|
|
58
63
|
"widgets:catalog:24h-ago": { de: "vor 24h", en: "24h ago" },
|
|
59
64
|
"widgets:catalog:12h-ago": { de: "vor 12h", en: "12h ago" },
|
|
60
65
|
"widgets:catalog:now": { de: "jetzt", en: "now" },
|
|
@@ -120,6 +120,17 @@ export function Widgets(): ReactNode {
|
|
|
120
120
|
end: t("widgets:catalog:now"),
|
|
121
121
|
}}
|
|
122
122
|
/>
|
|
123
|
+
<div className="mt-4">
|
|
124
|
+
<TimeseriesChart
|
|
125
|
+
points={RESPONSE_TIMES}
|
|
126
|
+
windowStartMs={0}
|
|
127
|
+
windowEndMs={24 * 60 * 60 * 1000}
|
|
128
|
+
ariaLabel={t("widgets:catalog:response-time-p95-aria")}
|
|
129
|
+
referenceLines={[
|
|
130
|
+
{ value: 180, label: t("widgets:catalog:response-time-p95"), tone: "warn" },
|
|
131
|
+
]}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
123
134
|
</SectionCard>
|
|
124
135
|
|
|
125
136
|
<SectionCard
|