@intx/db 0.2.2 → 0.3.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/README.md +2 -2
- package/dist/approval-store.d.ts +52 -0
- package/dist/approval-store.js +79 -0
- package/dist/asset-resolution.d.ts +2 -2
- package/dist/client.d.ts +10 -0
- package/dist/connection.js +11 -8
- package/dist/credential-resolution.d.ts +130 -18
- package/dist/credential-resolution.js +142 -3
- package/dist/grant-store.js +35 -13
- package/dist/index.d.ts +13 -4
- package/dist/index.js +11 -3
- package/dist/model-source-resolution.d.ts +52 -6
- package/dist/model-source-resolution.js +145 -19
- package/dist/parse-row.d.ts +212 -32
- package/dist/parse-row.js +109 -18
- package/dist/rekey-credential-secrets.d.ts +13 -0
- package/dist/rekey-credential-secrets.js +61 -0
- package/dist/schema/approvals.d.ts +245 -0
- package/dist/schema/approvals.js +52 -0
- package/dist/schema/catalog.d.ts +17 -0
- package/dist/schema/catalog.js +7 -1
- package/dist/schema/credentials.d.ts +1 -1
- package/dist/schema/git-tokens.js +1 -1
- package/dist/schema/grants.js +8 -2
- package/dist/schema/index.d.ts +8 -4
- package/dist/schema/index.js +8 -4
- package/dist/schema/messages.d.ts +5 -5
- package/dist/schema/messages.js +20 -9
- package/dist/schema/offerings.js +8 -2
- package/dist/schema/principals.d.ts +2 -2
- package/dist/schema/principals.js +7 -5
- package/dist/schema/providers.d.ts +17 -0
- package/dist/schema/providers.js +5 -0
- package/dist/schema/roles.js +6 -2
- package/dist/schema/session-assets.d.ts +1 -38
- package/dist/schema/session-assets.js +36 -19
- package/dist/schema/sessions.js +7 -2
- package/dist/schema/sidecar-allocation.d.ts +406 -0
- package/dist/schema/sidecar-allocation.js +57 -0
- package/dist/schema/sidecar.d.ts +19 -2
- package/dist/schema/sidecar.js +13 -3
- package/dist/schema/signal-correlations.d.ts +177 -0
- package/dist/schema/signal-correlations.js +31 -0
- package/dist/schema/wallets.d.ts +3 -3
- package/dist/schema/wallets.js +4 -2
- package/dist/schema/{agents.d.ts → workflow-definitions.d.ts} +79 -113
- package/dist/schema/workflow-definitions.js +97 -0
- package/dist/schema/workflow-run-dispatch.d.ts +328 -0
- package/dist/schema/workflow-run-dispatch.js +56 -0
- package/dist/schema/{workflow-deployments.d.ts → workflow-run-execution.d.ts} +43 -46
- package/dist/schema/workflow-run-execution.js +48 -0
- package/dist/schema/workflow-run-launch-spec.d.ts +200 -0
- package/dist/schema/workflow-run-launch-spec.js +26 -0
- package/dist/schema/{instances.d.ts → workflow-run.d.ts} +39 -71
- package/dist/schema/workflow-run.js +102 -0
- package/dist/sidecar-allocation-store.d.ts +171 -0
- package/dist/sidecar-allocation-store.js +512 -0
- package/dist/signal-correlation-store.d.ts +36 -0
- package/dist/signal-correlation-store.js +63 -0
- package/dist/tenant-hierarchy.d.ts +2 -1
- package/dist/tenant-hierarchy.js +10 -5
- package/dist/workflow-definition-store.d.ts +55 -0
- package/dist/workflow-definition-store.js +93 -0
- package/dist/workflow-run-dispatch-store.d.ts +80 -0
- package/dist/workflow-run-dispatch-store.js +298 -0
- package/dist/workflow-run-launch-spec-store.d.ts +13 -0
- package/dist/workflow-run-launch-spec-store.js +30 -0
- package/dist/workflow-run-store.d.ts +54 -0
- package/dist/workflow-run-store.js +85 -0
- package/migrations/0037_credential_use_backfill.sql +39 -0
- package/migrations/0038_chilly_blacklash.sql +37 -0
- package/migrations/0039_quick_carnage.sql +2 -0
- package/migrations/0040_reshape_approval_origin.sql +24 -0
- package/migrations/0041_make_approval_timeout_at_nullable.sql +1 -0
- package/migrations/0042_youthful_mantis.sql +2 -0
- package/migrations/0043_signal_correlation_deployment_fk.sql +1 -0
- package/migrations/0044_model_offering_quirks.sql +1 -0
- package/migrations/0045_create_workflow_run.sql +13 -0
- package/migrations/0046_approval_signal_correlation_run_fk.sql +2 -0
- package/migrations/0047_create_workflow_definition.sql +30 -0
- package/migrations/0048_workflow_definition_origin_agent_id.sql +2 -0
- package/migrations/0049_curly_omega_flight.sql +2 -0
- package/migrations/0050_late_crystal.sql +10 -0
- package/migrations/0051_funny_madelyne_pryor.sql +1 -0
- package/migrations/0052_drop_inference_turn_instance_fk.sql +1 -0
- package/migrations/0053_session_mail_session_id_index.sql +1 -0
- package/migrations/0054_rekey_instance_grants_to_workflow_run.sql +10 -0
- package/migrations/0055_backfill_anchor_workflow_runs.sql +69 -0
- package/migrations/0056_repoint_deployment_fks_to_anchor_run.sql +32 -0
- package/migrations/0057_drop_workflow_deployment_projection.sql +22 -0
- package/migrations/0058_repoint_offering_fk_to_definition.sql +41 -0
- package/migrations/0059_repoint_transaction_fk_to_run.sql +10 -0
- package/migrations/0060_drop_session_mail_instance_fk.sql +6 -0
- package/migrations/0061_drop_session_asset_instance_fk.sql +7 -0
- package/migrations/0062_drop_agent_asset_table.sql +11 -0
- package/migrations/0063_repoint_agent_role_fk_to_definition.sql +39 -0
- package/migrations/0064_repoint_agent_session_fk_to_definition.sql +47 -0
- package/migrations/0065_add_workflow_definition_model_requirements.sql +37 -0
- package/migrations/0066_rekey_agent_definition_principals_to_workflow.sql +27 -0
- package/migrations/0067_add_workflow_definition_kind.sql +10 -0
- package/migrations/0068_drop_agent_tables_and_origin_agent_id.sql +31 -0
- package/migrations/0069_drop_workflow_definition_kind.sql +10 -0
- package/migrations/0070_flashy_proteus.sql +13 -0
- package/migrations/0072_add_workflow_definition_credential_bindings.sql +1 -0
- package/migrations/0073_grant_target_exactly_one_check.sql +13 -0
- package/migrations/0074_add_provider_api_base_url.sql +1 -0
- package/migrations/0075_workflow_run_launch_spec.sql +17 -0
- package/migrations/0076_sidecar_allocation.sql +40 -0
- package/migrations/0077_workflow_run_dispatch.sql +29 -0
- package/migrations/0078_workflow_run_dispatch_kind.sql +2 -0
- package/migrations/0079_rename_workflow_run_deployment_id_to_anchor_run_id.sql +4 -0
- package/migrations/0080_rename_correlation_approval_deployment_id_to_anchor_run_id.sql +9 -0
- package/migrations/0081_workflow_definition_content_hash_and_approved_wire_hash.sql +4 -0
- package/migrations/0082_blue_black_queen.sql +1 -0
- package/migrations/0083_replace_launch_spec_snapshot_with_frozen_bundle.sql +3 -0
- package/migrations/0084_delete_orphaned_credential_grants.sql +22 -0
- package/migrations/meta/0037_snapshot.json +3237 -0
- package/migrations/meta/0038_snapshot.json +3470 -0
- package/migrations/meta/0039_snapshot.json +3470 -0
- package/migrations/meta/0040_snapshot.json +3489 -0
- package/migrations/meta/0041_snapshot.json +3489 -0
- package/migrations/meta/0042_snapshot.json +3489 -0
- package/migrations/meta/0043_snapshot.json +3498 -0
- package/migrations/meta/0044_snapshot.json +3504 -0
- package/migrations/meta/0045_snapshot.json +3589 -0
- package/migrations/meta/0046_snapshot.json +3607 -0
- package/migrations/meta/0047_snapshot.json +3832 -0
- package/migrations/meta/0048_snapshot.json +3853 -0
- package/migrations/meta/0049_snapshot.json +3854 -0
- package/migrations/meta/0050_snapshot.json +3924 -0
- package/migrations/meta/0051_snapshot.json +3940 -0
- package/migrations/meta/0052_snapshot.json +3931 -0
- package/migrations/meta/0053_snapshot.json +3952 -0
- package/migrations/meta/0054_snapshot.json +3952 -0
- package/migrations/meta/0055_snapshot.json +3952 -0
- package/migrations/meta/0056_snapshot.json +3952 -0
- package/migrations/meta/0057_snapshot.json +3839 -0
- package/migrations/meta/0058_snapshot.json +3839 -0
- package/migrations/meta/0059_snapshot.json +3839 -0
- package/migrations/meta/0060_snapshot.json +3830 -0
- package/migrations/meta/0061_snapshot.json +3821 -0
- package/migrations/meta/0062_snapshot.json +3723 -0
- package/migrations/meta/0063_snapshot.json +3723 -0
- package/migrations/meta/0064_snapshot.json +3723 -0
- package/migrations/meta/0065_snapshot.json +3729 -0
- package/migrations/meta/0066_snapshot.json +3729 -0
- package/migrations/meta/0067_snapshot.json +3736 -0
- package/migrations/meta/0068_snapshot.json +3347 -0
- package/migrations/meta/0069_snapshot.json +3340 -0
- package/migrations/meta/0070_snapshot.json +3444 -0
- package/migrations/meta/0072_snapshot.json +3450 -0
- package/migrations/meta/0073_snapshot.json +3455 -0
- package/migrations/meta/0074_snapshot.json +3461 -0
- package/migrations/meta/0075_snapshot.json +3567 -0
- package/migrations/meta/0076_snapshot.json +3851 -0
- package/migrations/meta/0077_snapshot.json +4067 -0
- package/migrations/meta/0078_snapshot.json +4078 -0
- package/migrations/meta/0079_snapshot.json +4078 -0
- package/migrations/meta/0080_snapshot.json +4078 -0
- package/migrations/meta/0081_snapshot.json +4096 -0
- package/migrations/meta/0082_snapshot.json +4102 -0
- package/migrations/meta/0083_snapshot.json +4096 -0
- package/migrations/meta/0084_snapshot.json +4096 -0
- package/migrations/meta/_journal.json +329 -0
- package/package.json +6 -3
- package/dist/schema/agent-assets.d.ts +0 -109
- package/dist/schema/agent-assets.js +0 -15
- package/dist/schema/agents.js +0 -57
- package/dist/schema/instances.js +0 -39
- package/dist/schema/workflow-deployments.js +0 -37
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
2
|
+
import { signalCorrelation } from "./schema/signal-correlations.js";
|
|
3
|
+
import { parseSignalCorrelationRow } from "./parse-row.js";
|
|
4
|
+
type DBHandle = DB["db"];
|
|
5
|
+
type SignalCorrelationInsert = typeof signalCorrelation.$inferInsert;
|
|
6
|
+
type ParsedSignalCorrelation = ReturnType<typeof parseSignalCorrelationRow>;
|
|
7
|
+
/**
|
|
8
|
+
* Store for the `signal_correlation` table, which maps an in-flight signal
|
|
9
|
+
* (correlation id) to the run and address that must be resumed when the signal
|
|
10
|
+
* resolves. Each method accepts an optional transaction handle so the resolver
|
|
11
|
+
* can claim the correlation and flip its approval atomically.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createSignalCorrelationStore(db: DBHandle): {
|
|
14
|
+
register(row: SignalCorrelationInsert, tx?: DBExecutor): Promise<ParsedSignalCorrelation>;
|
|
15
|
+
/**
|
|
16
|
+
* Idempotent variant of `register`. On a `correlationId` primary-key
|
|
17
|
+
* conflict the insert is a no-op and this returns `null` rather than
|
|
18
|
+
* throwing, so a redelivered register frame (sidecar reconnect,
|
|
19
|
+
* workflow-log replay, supervisor restart re-emitting) does not fail the
|
|
20
|
+
* co-write. Returns the parsed row only when this call performed the
|
|
21
|
+
* insert.
|
|
22
|
+
*/
|
|
23
|
+
registerIfAbsent(row: SignalCorrelationInsert, tx?: DBExecutor): Promise<ParsedSignalCorrelation | null>;
|
|
24
|
+
resolveRoute(correlationId: string, tx?: DBExecutor): Promise<ParsedSignalCorrelation | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Atomically claim a correlation for terminal delivery. The
|
|
27
|
+
* `resolved_at IS NULL` guard makes the claim single-shot: the first caller
|
|
28
|
+
* stamps `resolvedAt` and gets the row back; any second caller matches no
|
|
29
|
+
* row and receives null, so a redelivered signal is not delivered twice.
|
|
30
|
+
* When `signalId` is provided it is persisted on the same update, recording
|
|
31
|
+
* which signal instance won the claim for redelivery idempotency.
|
|
32
|
+
*/
|
|
33
|
+
claimTerminal(correlationId: string, resolvedAt: Date, signalId: string | null, tx?: DBExecutor): Promise<ParsedSignalCorrelation | null>;
|
|
34
|
+
};
|
|
35
|
+
export type SignalCorrelationStore = ReturnType<typeof createSignalCorrelationStore>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
2
|
+
import { signalCorrelation } from "./schema/signal-correlations.js";
|
|
3
|
+
import { parseSignalCorrelationRow } from "./parse-row.js";
|
|
4
|
+
/**
|
|
5
|
+
* Store for the `signal_correlation` table, which maps an in-flight signal
|
|
6
|
+
* (correlation id) to the run and address that must be resumed when the signal
|
|
7
|
+
* resolves. Each method accepts an optional transaction handle so the resolver
|
|
8
|
+
* can claim the correlation and flip its approval atomically.
|
|
9
|
+
*/
|
|
10
|
+
export function createSignalCorrelationStore(db) {
|
|
11
|
+
return {
|
|
12
|
+
async register(row, tx) {
|
|
13
|
+
const [inserted] = await (tx ?? db)
|
|
14
|
+
.insert(signalCorrelation)
|
|
15
|
+
.values(row)
|
|
16
|
+
.returning();
|
|
17
|
+
if (inserted === undefined) {
|
|
18
|
+
throw new Error(`signalCorrelationStore.register: insert returned no row for ${row.correlationId}`);
|
|
19
|
+
}
|
|
20
|
+
return parseSignalCorrelationRow(inserted);
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Idempotent variant of `register`. On a `correlationId` primary-key
|
|
24
|
+
* conflict the insert is a no-op and this returns `null` rather than
|
|
25
|
+
* throwing, so a redelivered register frame (sidecar reconnect,
|
|
26
|
+
* workflow-log replay, supervisor restart re-emitting) does not fail the
|
|
27
|
+
* co-write. Returns the parsed row only when this call performed the
|
|
28
|
+
* insert.
|
|
29
|
+
*/
|
|
30
|
+
async registerIfAbsent(row, tx) {
|
|
31
|
+
const [inserted] = await (tx ?? db)
|
|
32
|
+
.insert(signalCorrelation)
|
|
33
|
+
.values(row)
|
|
34
|
+
.onConflictDoNothing({ target: signalCorrelation.correlationId })
|
|
35
|
+
.returning();
|
|
36
|
+
return inserted === undefined
|
|
37
|
+
? null
|
|
38
|
+
: parseSignalCorrelationRow(inserted);
|
|
39
|
+
},
|
|
40
|
+
async resolveRoute(correlationId, tx) {
|
|
41
|
+
const row = await (tx ?? db).query.signalCorrelation.findFirst({
|
|
42
|
+
where: eq(signalCorrelation.correlationId, correlationId),
|
|
43
|
+
});
|
|
44
|
+
return row === undefined ? null : parseSignalCorrelationRow(row);
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* Atomically claim a correlation for terminal delivery. The
|
|
48
|
+
* `resolved_at IS NULL` guard makes the claim single-shot: the first caller
|
|
49
|
+
* stamps `resolvedAt` and gets the row back; any second caller matches no
|
|
50
|
+
* row and receives null, so a redelivered signal is not delivered twice.
|
|
51
|
+
* When `signalId` is provided it is persisted on the same update, recording
|
|
52
|
+
* which signal instance won the claim for redelivery idempotency.
|
|
53
|
+
*/
|
|
54
|
+
async claimTerminal(correlationId, resolvedAt, signalId, tx) {
|
|
55
|
+
const [claimed] = await (tx ?? db)
|
|
56
|
+
.update(signalCorrelation)
|
|
57
|
+
.set({ resolvedAt, signalId })
|
|
58
|
+
.where(and(eq(signalCorrelation.correlationId, correlationId), isNull(signalCorrelation.resolvedAt)))
|
|
59
|
+
.returning();
|
|
60
|
+
return claimed === undefined ? null : parseSignalCorrelationRow(claimed);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -3,7 +3,8 @@ import type { DB } from "./client.js";
|
|
|
3
3
|
* Walks the tenant parentId chain from the given tenant up to the root.
|
|
4
4
|
* Returns an ordered array of tenant IDs: [tenantId, parentId, grandparentId, ...rootId].
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Throws if the hierarchy contains a cycle instead of returning a partial
|
|
7
|
+
* chain that could omit inherited policy or configuration.
|
|
7
8
|
*/
|
|
8
9
|
export declare function getAncestorChain(db: DB["db"], tenantId: string): Promise<string[]>;
|
|
9
10
|
/**
|
package/dist/tenant-hierarchy.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { eq, inArray } from "drizzle-orm";
|
|
2
2
|
import { tenant } from "./schema/tenants.js";
|
|
3
|
-
const MAX_DEPTH = 20;
|
|
4
3
|
/**
|
|
5
4
|
* Walks the tenant parentId chain from the given tenant up to the root.
|
|
6
5
|
* Returns an ordered array of tenant IDs: [tenantId, parentId, grandparentId, ...rootId].
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Throws if the hierarchy contains a cycle instead of returning a partial
|
|
8
|
+
* chain that could omit inherited policy or configuration.
|
|
9
9
|
*/
|
|
10
10
|
export async function getAncestorChain(db, tenantId) {
|
|
11
|
-
const chain = [
|
|
11
|
+
const chain = [];
|
|
12
|
+
const visited = new Set();
|
|
12
13
|
let currentId = tenantId;
|
|
13
|
-
|
|
14
|
+
while (true) {
|
|
15
|
+
if (visited.has(currentId)) {
|
|
16
|
+
throw new Error(`Tenant hierarchy contains a cycle at tenant ${currentId}`);
|
|
17
|
+
}
|
|
18
|
+
visited.add(currentId);
|
|
19
|
+
chain.push(currentId);
|
|
14
20
|
const row = await db.query.tenant.findFirst({
|
|
15
21
|
where: eq(tenant.id, currentId),
|
|
16
22
|
columns: { parentId: true },
|
|
17
23
|
});
|
|
18
24
|
if (!row?.parentId)
|
|
19
25
|
break;
|
|
20
|
-
chain.push(row.parentId);
|
|
21
26
|
currentId = row.parentId;
|
|
22
27
|
}
|
|
23
28
|
return chain;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { GrantWalkSnapshot } from "@intx/types";
|
|
2
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
3
|
+
import { parseWorkflowDefinitionRow } from "./parse-row.js";
|
|
4
|
+
type DBHandle = DB["db"];
|
|
5
|
+
type ParsedWorkflowDefinition = ReturnType<typeof parseWorkflowDefinitionRow>;
|
|
6
|
+
/**
|
|
7
|
+
* The selector that keys a workflow definition's identity: the asset it
|
|
8
|
+
* projects and the content hash of its wire projection. A single asset backs
|
|
9
|
+
* many definitions distinguished by their wire hash, so both fields are
|
|
10
|
+
* required to name exactly one definition.
|
|
11
|
+
*/
|
|
12
|
+
export type WorkflowDefinitionSelector = {
|
|
13
|
+
assetId: string;
|
|
14
|
+
wireHash: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The definition a selector names, or null when no definition has been folded
|
|
18
|
+
* for that `(assetId, wireHash)` pair. This is the single expression of the
|
|
19
|
+
* deployment -> definition mapping (a deployment names its asset and wire hash);
|
|
20
|
+
* the run backfill and the native-run insert sites both resolve through it.
|
|
21
|
+
* Null on a miss is deliberate, not an error: a deployment whose selector the
|
|
22
|
+
* run-once fold never covered has no definition yet, and its runs anchor on
|
|
23
|
+
* `runId` until that gap closes.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveDefinitionIdForAsset(db: DBExecutor, selector: WorkflowDefinitionSelector): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Read the deploy-approved grant-walk snapshot frozen onto a definition's
|
|
28
|
+
* version row, validated as a `GrantWalkSnapshot` at this boundary. Returns
|
|
29
|
+
* `null` when the version row is absent or its `grantSnapshot` column is still
|
|
30
|
+
* `null` -- the "not yet approved" state, mirroring `approvedWireHash`. The
|
|
31
|
+
* caller fails closed on `null`; it never substitutes an empty grant set.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadFrozenGrantSnapshot(db: DBExecutor, definitionId: string): Promise<GrantWalkSnapshot | null>;
|
|
34
|
+
export type WorkflowDefinitionRollbackResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
definition: ParsedWorkflowDefinition;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: "definition_not_found" | "version_not_found";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Store for the first-class `workflow_definition` and its version history.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createWorkflowDefinitionStore(db: DBHandle): {
|
|
45
|
+
/**
|
|
46
|
+
* Roll a definition back to a prior version: deactivate the current
|
|
47
|
+
* version, activate the target, and repoint `currentVersion` in one
|
|
48
|
+
* transaction, so a reader never sees two active versions or a
|
|
49
|
+
* `currentVersion` that names an inactive row. Returns a discriminated
|
|
50
|
+
* result rather than throwing for the not-found cases the route reports as
|
|
51
|
+
* 404 / 400.
|
|
52
|
+
*/
|
|
53
|
+
rollback(tenantId: string, definitionId: string, targetVersion: string): Promise<WorkflowDefinitionRollbackResult>;
|
|
54
|
+
};
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { GrantWalkSnapshot } from "@intx/types";
|
|
3
|
+
import { workflowDefinition, workflowDefinitionVersion, } from "./schema/workflow-definitions.js";
|
|
4
|
+
import { parseWorkflowDefinitionRow } from "./parse-row.js";
|
|
5
|
+
// The version `ensureWorkflowDefinitionForAsset` projects for a fresh
|
|
6
|
+
// definition, and therefore the row the approval freeze stamps the grant
|
|
7
|
+
// snapshot onto. Hand-coupled to the ensure helper's initial version; if that
|
|
8
|
+
// helper ever projects a different version this must follow.
|
|
9
|
+
const FROZEN_VERSION = "1";
|
|
10
|
+
/**
|
|
11
|
+
* The definition a selector names, or null when no definition has been folded
|
|
12
|
+
* for that `(assetId, wireHash)` pair. This is the single expression of the
|
|
13
|
+
* deployment -> definition mapping (a deployment names its asset and wire hash);
|
|
14
|
+
* the run backfill and the native-run insert sites both resolve through it.
|
|
15
|
+
* Null on a miss is deliberate, not an error: a deployment whose selector the
|
|
16
|
+
* run-once fold never covered has no definition yet, and its runs anchor on
|
|
17
|
+
* `runId` until that gap closes.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveDefinitionIdForAsset(db, selector) {
|
|
20
|
+
const row = await db
|
|
21
|
+
.select({ id: workflowDefinition.id })
|
|
22
|
+
.from(workflowDefinition)
|
|
23
|
+
.where(and(eq(workflowDefinition.assetId, selector.assetId), eq(workflowDefinition.wireHash, selector.wireHash)))
|
|
24
|
+
.limit(1)
|
|
25
|
+
.then((rows) => rows[0]);
|
|
26
|
+
return row?.id ?? null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read the deploy-approved grant-walk snapshot frozen onto a definition's
|
|
30
|
+
* version row, validated as a `GrantWalkSnapshot` at this boundary. Returns
|
|
31
|
+
* `null` when the version row is absent or its `grantSnapshot` column is still
|
|
32
|
+
* `null` -- the "not yet approved" state, mirroring `approvedWireHash`. The
|
|
33
|
+
* caller fails closed on `null`; it never substitutes an empty grant set.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadFrozenGrantSnapshot(db, definitionId) {
|
|
36
|
+
const row = await db
|
|
37
|
+
.select({ grantSnapshot: workflowDefinitionVersion.grantSnapshot })
|
|
38
|
+
.from(workflowDefinitionVersion)
|
|
39
|
+
.where(and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, FROZEN_VERSION)))
|
|
40
|
+
.limit(1)
|
|
41
|
+
.then((rows) => rows[0]);
|
|
42
|
+
if (row === undefined || row.grantSnapshot === null)
|
|
43
|
+
return null;
|
|
44
|
+
return GrantWalkSnapshot.assert(row.grantSnapshot);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Store for the first-class `workflow_definition` and its version history.
|
|
48
|
+
*/
|
|
49
|
+
export function createWorkflowDefinitionStore(db) {
|
|
50
|
+
return {
|
|
51
|
+
/**
|
|
52
|
+
* Roll a definition back to a prior version: deactivate the current
|
|
53
|
+
* version, activate the target, and repoint `currentVersion` in one
|
|
54
|
+
* transaction, so a reader never sees two active versions or a
|
|
55
|
+
* `currentVersion` that names an inactive row. Returns a discriminated
|
|
56
|
+
* result rather than throwing for the not-found cases the route reports as
|
|
57
|
+
* 404 / 400.
|
|
58
|
+
*/
|
|
59
|
+
async rollback(tenantId, definitionId, targetVersion) {
|
|
60
|
+
return db.transaction(async (tx) => {
|
|
61
|
+
const existing = await tx.query.workflowDefinition.findFirst({
|
|
62
|
+
where: and(eq(workflowDefinition.id, definitionId), eq(workflowDefinition.tenantId, tenantId)),
|
|
63
|
+
});
|
|
64
|
+
if (existing === undefined) {
|
|
65
|
+
return { ok: false, reason: "definition_not_found" };
|
|
66
|
+
}
|
|
67
|
+
const target = await tx.query.workflowDefinitionVersion.findFirst({
|
|
68
|
+
where: and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, targetVersion)),
|
|
69
|
+
});
|
|
70
|
+
if (target === undefined) {
|
|
71
|
+
return { ok: false, reason: "version_not_found" };
|
|
72
|
+
}
|
|
73
|
+
await tx
|
|
74
|
+
.update(workflowDefinitionVersion)
|
|
75
|
+
.set({ status: "inactive" })
|
|
76
|
+
.where(and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, existing.currentVersion)));
|
|
77
|
+
await tx
|
|
78
|
+
.update(workflowDefinitionVersion)
|
|
79
|
+
.set({ status: "active" })
|
|
80
|
+
.where(and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, targetVersion)));
|
|
81
|
+
const [updated] = await tx
|
|
82
|
+
.update(workflowDefinition)
|
|
83
|
+
.set({ currentVersion: targetVersion, updatedAt: new Date() })
|
|
84
|
+
.where(eq(workflowDefinition.id, definitionId))
|
|
85
|
+
.returning();
|
|
86
|
+
if (updated === undefined) {
|
|
87
|
+
throw new Error(`workflowDefinitionStore.rollback: update returned no row for ${definitionId}`);
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, definition: parseWorkflowDefinitionRow(updated) };
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type SQL } from "drizzle-orm";
|
|
2
|
+
import { type RunGrantsFrame as RunGrantsFrameType, type SignalDeliverFrame as SignalDeliverFrameType } from "@intx/types/sidecar";
|
|
3
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
4
|
+
import { parseWorkflowRunDispatchRow } from "./parse-row.js";
|
|
5
|
+
type DBHandle = DB["db"];
|
|
6
|
+
type ParsedDispatch = ReturnType<typeof parseWorkflowRunDispatchRow>;
|
|
7
|
+
export type EnqueueWorkflowRunDispatchArgs = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly anchorRunId: string;
|
|
10
|
+
readonly messageId: string;
|
|
11
|
+
readonly rawMessage: Uint8Array;
|
|
12
|
+
readonly stepGrants: RunGrantsFrameType["stepGrants"];
|
|
13
|
+
readonly now?: Date;
|
|
14
|
+
};
|
|
15
|
+
export type EnqueueWorkflowSignalDispatchArgs = {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly anchorRunId: string;
|
|
18
|
+
readonly signal: Omit<SignalDeliverFrameType, "type">;
|
|
19
|
+
readonly now?: Date;
|
|
20
|
+
};
|
|
21
|
+
export type EnqueueWorkflowRunDispatchResult = {
|
|
22
|
+
readonly dispatch: ParsedDispatch;
|
|
23
|
+
readonly created: boolean;
|
|
24
|
+
};
|
|
25
|
+
type InsertOrReconcileDispatchArgs = {
|
|
26
|
+
readonly operation: "enqueue" | "enqueueSignal";
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly anchorRunId: string;
|
|
29
|
+
readonly messageId: string;
|
|
30
|
+
readonly kind: ParsedDispatch["kind"];
|
|
31
|
+
readonly rawMessage: Uint8Array;
|
|
32
|
+
readonly stepGrants: RunGrantsFrameType["stepGrants"];
|
|
33
|
+
readonly now?: Date;
|
|
34
|
+
};
|
|
35
|
+
export declare class WorkflowRunDispatchPayloadConflictError extends Error {
|
|
36
|
+
readonly messageId: string;
|
|
37
|
+
readonly payloadKind: "message" | "signal";
|
|
38
|
+
constructor(messageId: string, payloadKind: "message" | "signal", operation: InsertOrReconcileDispatchArgs["operation"]);
|
|
39
|
+
}
|
|
40
|
+
export type ClaimWorkflowRunDispatchArgs = {
|
|
41
|
+
readonly leaseId: string;
|
|
42
|
+
readonly leaseDurationMs: number;
|
|
43
|
+
};
|
|
44
|
+
export type AcknowledgeWorkflowRunDispatchArgs = {
|
|
45
|
+
readonly allocationId: string;
|
|
46
|
+
readonly anchorRunId: string;
|
|
47
|
+
readonly messageId: string;
|
|
48
|
+
readonly generation: number;
|
|
49
|
+
readonly now?: Date;
|
|
50
|
+
};
|
|
51
|
+
export type RetryWorkflowRunDispatchArgs = {
|
|
52
|
+
readonly dispatchId: string;
|
|
53
|
+
readonly nextAttemptAt: Date;
|
|
54
|
+
readonly code: string;
|
|
55
|
+
readonly message: string;
|
|
56
|
+
readonly expectedLeaseId?: string;
|
|
57
|
+
readonly now?: Date;
|
|
58
|
+
};
|
|
59
|
+
export type FailWorkflowRunDispatchArgs = {
|
|
60
|
+
readonly anchorRunId: string;
|
|
61
|
+
readonly messageId: string;
|
|
62
|
+
readonly code: string;
|
|
63
|
+
readonly message: string;
|
|
64
|
+
readonly now?: Date;
|
|
65
|
+
};
|
|
66
|
+
export declare function createWorkflowRunDispatchStore(db: DBHandle): {
|
|
67
|
+
enqueue(args: EnqueueWorkflowRunDispatchArgs, tx?: DBExecutor): Promise<EnqueueWorkflowRunDispatchResult>;
|
|
68
|
+
enqueueSignal(args: EnqueueWorkflowSignalDispatchArgs, tx?: DBExecutor): Promise<EnqueueWorkflowRunDispatchResult>;
|
|
69
|
+
claimNextPending(args: ClaimWorkflowRunDispatchArgs): Promise<ParsedDispatch | null>;
|
|
70
|
+
acknowledge(args: AcknowledgeWorkflowRunDispatchArgs): Promise<ParsedDispatch | null>;
|
|
71
|
+
scheduleRetry(args: RetryWorkflowRunDispatchArgs): Promise<ParsedDispatch | null>;
|
|
72
|
+
settle(anchorRunId: string, messageId: string, now?: Date): Promise<ParsedDispatch | null>;
|
|
73
|
+
fail(args: FailWorkflowRunDispatchArgs): Promise<ParsedDispatch | null>;
|
|
74
|
+
failUnsettled(anchorRunId: string, code: string, message: string, now?: Date | SQL, tx?: DBExecutor): Promise<number>;
|
|
75
|
+
requeueUnsettled(anchorRunId: string): Promise<number>;
|
|
76
|
+
findById(id: string): Promise<ParsedDispatch | null>;
|
|
77
|
+
listUnsettled(anchorRunId: string): Promise<ParsedDispatch[]>;
|
|
78
|
+
};
|
|
79
|
+
export type WorkflowRunDispatchStore = ReturnType<typeof createWorkflowRunDispatchStore>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { and, asc, eq, inArray, isNull, lte, or, sql, } from "drizzle-orm";
|
|
3
|
+
import { RunGrantsFrame, SignalDeliverFrame, } from "@intx/types/sidecar";
|
|
4
|
+
import { parseWorkflowRunDispatchRow } from "./parse-row.js";
|
|
5
|
+
import { sidecarAllocation, workflowRun, workflowRunDispatch } from "./schema/index.js";
|
|
6
|
+
export class WorkflowRunDispatchPayloadConflictError extends Error {
|
|
7
|
+
messageId;
|
|
8
|
+
payloadKind;
|
|
9
|
+
constructor(messageId, payloadKind, operation) {
|
|
10
|
+
super(`workflowRunDispatchStore.${operation}: ${payloadKind} ${messageId} conflicts with its durable payload`);
|
|
11
|
+
this.messageId = messageId;
|
|
12
|
+
this.payloadKind = payloadKind;
|
|
13
|
+
this.name = "WorkflowRunDispatchPayloadConflictError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function databaseTimestamp(override) {
|
|
17
|
+
return override ?? sql `now()`;
|
|
18
|
+
}
|
|
19
|
+
function leaseCondition(expectedLeaseId) {
|
|
20
|
+
return expectedLeaseId === undefined
|
|
21
|
+
? []
|
|
22
|
+
: [eq(workflowRunDispatch.deliveryLeaseId, expectedLeaseId)];
|
|
23
|
+
}
|
|
24
|
+
function byteArraysEqual(left, right) {
|
|
25
|
+
if (left.byteLength !== right.byteLength)
|
|
26
|
+
return false;
|
|
27
|
+
return left.every((value, index) => value === right[index]);
|
|
28
|
+
}
|
|
29
|
+
function grantsEqual(left, right) {
|
|
30
|
+
return isDeepStrictEqual(left, right);
|
|
31
|
+
}
|
|
32
|
+
function validateStepGrants(anchorRunId, stepGrants) {
|
|
33
|
+
return RunGrantsFrame.assert({
|
|
34
|
+
type: "run.grants",
|
|
35
|
+
agentAddress: "persisted@validation.invalid",
|
|
36
|
+
runId: anchorRunId,
|
|
37
|
+
stepGrants,
|
|
38
|
+
}).stepGrants;
|
|
39
|
+
}
|
|
40
|
+
async function insertOrReconcileDispatch(executor, args) {
|
|
41
|
+
const [anchor] = await executor
|
|
42
|
+
.select({ anchorRunId: workflowRun.anchorRunId })
|
|
43
|
+
.from(workflowRun)
|
|
44
|
+
.where(eq(workflowRun.id, args.anchorRunId))
|
|
45
|
+
.limit(1);
|
|
46
|
+
if (anchor?.anchorRunId !== args.anchorRunId) {
|
|
47
|
+
throw new Error(`workflowRunDispatchStore.${args.operation}: run ${args.anchorRunId} is not a deployment anchor`);
|
|
48
|
+
}
|
|
49
|
+
const now = databaseTimestamp(args.now);
|
|
50
|
+
const [inserted] = await executor
|
|
51
|
+
.insert(workflowRunDispatch)
|
|
52
|
+
.values({
|
|
53
|
+
id: args.id,
|
|
54
|
+
anchorRunId: args.anchorRunId,
|
|
55
|
+
messageId: args.messageId,
|
|
56
|
+
kind: args.kind,
|
|
57
|
+
rawMessage: args.rawMessage,
|
|
58
|
+
stepGrants: args.stepGrants,
|
|
59
|
+
status: "pending",
|
|
60
|
+
nextAttemptAt: now,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
})
|
|
64
|
+
.onConflictDoNothing({
|
|
65
|
+
target: [workflowRunDispatch.anchorRunId, workflowRunDispatch.messageId],
|
|
66
|
+
})
|
|
67
|
+
.returning();
|
|
68
|
+
if (inserted !== undefined) {
|
|
69
|
+
return {
|
|
70
|
+
dispatch: parseWorkflowRunDispatchRow(inserted),
|
|
71
|
+
created: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const existing = await executor.query.workflowRunDispatch.findFirst({
|
|
75
|
+
where: and(eq(workflowRunDispatch.anchorRunId, args.anchorRunId), eq(workflowRunDispatch.messageId, args.messageId)),
|
|
76
|
+
});
|
|
77
|
+
if (existing === undefined) {
|
|
78
|
+
throw new Error(`workflowRunDispatchStore.${args.operation}: conflicting row disappeared for ${args.anchorRunId}/${args.messageId}`);
|
|
79
|
+
}
|
|
80
|
+
const parsed = parseWorkflowRunDispatchRow(existing);
|
|
81
|
+
if (parsed.kind !== args.kind ||
|
|
82
|
+
!byteArraysEqual(parsed.rawMessage, args.rawMessage) ||
|
|
83
|
+
!grantsEqual(parsed.stepGrants, args.stepGrants)) {
|
|
84
|
+
const payloadKind = args.kind === "mail" ? "message" : "signal";
|
|
85
|
+
throw new WorkflowRunDispatchPayloadConflictError(args.messageId, payloadKind, args.operation);
|
|
86
|
+
}
|
|
87
|
+
return { dispatch: parsed, created: false };
|
|
88
|
+
}
|
|
89
|
+
async function acknowledgeMatchingDispatch(executor, generation, conditions, nowOverride) {
|
|
90
|
+
const now = databaseTimestamp(nowOverride);
|
|
91
|
+
const [updated] = await executor
|
|
92
|
+
.update(workflowRunDispatch)
|
|
93
|
+
.set({
|
|
94
|
+
status: "acknowledged",
|
|
95
|
+
acknowledgedGeneration: generation,
|
|
96
|
+
acknowledgedAt: now,
|
|
97
|
+
attemptCount: sql `${workflowRunDispatch.attemptCount} + 1`,
|
|
98
|
+
nextAttemptAt: null,
|
|
99
|
+
deliveryLeaseId: null,
|
|
100
|
+
deliveryLeaseExpiresAt: null,
|
|
101
|
+
failureCode: null,
|
|
102
|
+
failureMessage: null,
|
|
103
|
+
updatedAt: now,
|
|
104
|
+
})
|
|
105
|
+
.where(and(...conditions))
|
|
106
|
+
.returning();
|
|
107
|
+
return updated === undefined ? null : parseWorkflowRunDispatchRow(updated);
|
|
108
|
+
}
|
|
109
|
+
export function createWorkflowRunDispatchStore(db) {
|
|
110
|
+
return {
|
|
111
|
+
async enqueue(args, tx) {
|
|
112
|
+
const stepGrants = validateStepGrants(args.anchorRunId, args.stepGrants);
|
|
113
|
+
return insertOrReconcileDispatch(tx ?? db, {
|
|
114
|
+
operation: "enqueue",
|
|
115
|
+
id: args.id,
|
|
116
|
+
anchorRunId: args.anchorRunId,
|
|
117
|
+
messageId: args.messageId,
|
|
118
|
+
kind: "mail",
|
|
119
|
+
rawMessage: args.rawMessage,
|
|
120
|
+
stepGrants,
|
|
121
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
async enqueueSignal(args, tx) {
|
|
125
|
+
const signal = SignalDeliverFrame.assert({
|
|
126
|
+
type: "signal.deliver",
|
|
127
|
+
...args.signal,
|
|
128
|
+
});
|
|
129
|
+
const encoded = new TextEncoder().encode(JSON.stringify(signal));
|
|
130
|
+
return insertOrReconcileDispatch(tx ?? db, {
|
|
131
|
+
operation: "enqueueSignal",
|
|
132
|
+
id: args.id,
|
|
133
|
+
anchorRunId: args.anchorRunId,
|
|
134
|
+
messageId: signal.signalId,
|
|
135
|
+
kind: "signal",
|
|
136
|
+
rawMessage: encoded,
|
|
137
|
+
stepGrants: [],
|
|
138
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
async claimNextPending(args) {
|
|
142
|
+
return db.transaction(async (tx) => {
|
|
143
|
+
const [candidate] = await tx
|
|
144
|
+
.select({ id: workflowRunDispatch.id })
|
|
145
|
+
.from(workflowRunDispatch)
|
|
146
|
+
.where(and(eq(workflowRunDispatch.status, "pending"), lte(workflowRunDispatch.nextAttemptAt, sql `now()`), or(isNull(workflowRunDispatch.deliveryLeaseExpiresAt), lte(workflowRunDispatch.deliveryLeaseExpiresAt, sql `now()`))))
|
|
147
|
+
.orderBy(asc(workflowRunDispatch.nextAttemptAt), asc(workflowRunDispatch.createdAt))
|
|
148
|
+
.limit(1)
|
|
149
|
+
.for("update", { skipLocked: true });
|
|
150
|
+
if (candidate === undefined)
|
|
151
|
+
return null;
|
|
152
|
+
const [claimed] = await tx
|
|
153
|
+
.update(workflowRunDispatch)
|
|
154
|
+
.set({
|
|
155
|
+
deliveryLeaseId: args.leaseId,
|
|
156
|
+
deliveryLeaseExpiresAt: sql `now() + (${args.leaseDurationMs} * interval '1 millisecond')`,
|
|
157
|
+
})
|
|
158
|
+
.where(eq(workflowRunDispatch.id, candidate.id))
|
|
159
|
+
.returning();
|
|
160
|
+
return claimed === undefined
|
|
161
|
+
? null
|
|
162
|
+
: parseWorkflowRunDispatchRow(claimed);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
async acknowledge(args) {
|
|
166
|
+
return db.transaction(async (tx) => {
|
|
167
|
+
const [allocation] = await tx
|
|
168
|
+
.select({
|
|
169
|
+
anchorRunId: sidecarAllocation.anchorRunId,
|
|
170
|
+
status: sidecarAllocation.status,
|
|
171
|
+
generation: sidecarAllocation.generation,
|
|
172
|
+
ensureAcceptedGeneration: sidecarAllocation.ensureAcceptedGeneration,
|
|
173
|
+
})
|
|
174
|
+
.from(sidecarAllocation)
|
|
175
|
+
.where(eq(sidecarAllocation.id, args.allocationId))
|
|
176
|
+
.limit(1)
|
|
177
|
+
.for("update");
|
|
178
|
+
if (allocation === undefined ||
|
|
179
|
+
allocation.anchorRunId !== args.anchorRunId ||
|
|
180
|
+
allocation.status !== "allocated" ||
|
|
181
|
+
allocation.generation !== args.generation ||
|
|
182
|
+
allocation.ensureAcceptedGeneration !== args.generation) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return acknowledgeMatchingDispatch(tx, args.generation, [
|
|
186
|
+
eq(workflowRunDispatch.anchorRunId, args.anchorRunId),
|
|
187
|
+
eq(workflowRunDispatch.messageId, args.messageId),
|
|
188
|
+
eq(workflowRunDispatch.kind, "mail"),
|
|
189
|
+
eq(workflowRunDispatch.status, "pending"),
|
|
190
|
+
], args.now);
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async scheduleRetry(args) {
|
|
194
|
+
const [updated] = await db
|
|
195
|
+
.update(workflowRunDispatch)
|
|
196
|
+
.set({
|
|
197
|
+
nextAttemptAt: args.nextAttemptAt,
|
|
198
|
+
attemptCount: sql `${workflowRunDispatch.attemptCount} + 1`,
|
|
199
|
+
deliveryLeaseId: null,
|
|
200
|
+
deliveryLeaseExpiresAt: null,
|
|
201
|
+
failureCode: args.code,
|
|
202
|
+
failureMessage: args.message,
|
|
203
|
+
updatedAt: databaseTimestamp(args.now),
|
|
204
|
+
})
|
|
205
|
+
.where(and(eq(workflowRunDispatch.id, args.dispatchId), eq(workflowRunDispatch.status, "pending"), ...leaseCondition(args.expectedLeaseId)))
|
|
206
|
+
.returning();
|
|
207
|
+
return updated === undefined
|
|
208
|
+
? null
|
|
209
|
+
: parseWorkflowRunDispatchRow(updated);
|
|
210
|
+
},
|
|
211
|
+
async settle(anchorRunId, messageId, now = new Date()) {
|
|
212
|
+
const [updated] = await db
|
|
213
|
+
.update(workflowRunDispatch)
|
|
214
|
+
.set({
|
|
215
|
+
status: "settled",
|
|
216
|
+
nextAttemptAt: null,
|
|
217
|
+
deliveryLeaseId: null,
|
|
218
|
+
deliveryLeaseExpiresAt: null,
|
|
219
|
+
failureCode: null,
|
|
220
|
+
failureMessage: null,
|
|
221
|
+
settledAt: now,
|
|
222
|
+
updatedAt: now,
|
|
223
|
+
})
|
|
224
|
+
.where(and(eq(workflowRunDispatch.anchorRunId, anchorRunId), eq(workflowRunDispatch.messageId, messageId), inArray(workflowRunDispatch.status, ["pending", "acknowledged"])))
|
|
225
|
+
.returning();
|
|
226
|
+
return updated === undefined
|
|
227
|
+
? null
|
|
228
|
+
: parseWorkflowRunDispatchRow(updated);
|
|
229
|
+
},
|
|
230
|
+
async fail(args) {
|
|
231
|
+
const now = args.now ?? new Date();
|
|
232
|
+
const [updated] = await db
|
|
233
|
+
.update(workflowRunDispatch)
|
|
234
|
+
.set({
|
|
235
|
+
status: "failed",
|
|
236
|
+
nextAttemptAt: null,
|
|
237
|
+
deliveryLeaseId: null,
|
|
238
|
+
deliveryLeaseExpiresAt: null,
|
|
239
|
+
failureCode: args.code,
|
|
240
|
+
failureMessage: args.message,
|
|
241
|
+
updatedAt: now,
|
|
242
|
+
})
|
|
243
|
+
.where(and(eq(workflowRunDispatch.anchorRunId, args.anchorRunId), eq(workflowRunDispatch.messageId, args.messageId), inArray(workflowRunDispatch.status, ["pending", "acknowledged"])))
|
|
244
|
+
.returning();
|
|
245
|
+
return updated === undefined
|
|
246
|
+
? null
|
|
247
|
+
: parseWorkflowRunDispatchRow(updated);
|
|
248
|
+
},
|
|
249
|
+
async failUnsettled(anchorRunId, code, message, now = new Date(), tx) {
|
|
250
|
+
const rows = await (tx ?? db)
|
|
251
|
+
.update(workflowRunDispatch)
|
|
252
|
+
.set({
|
|
253
|
+
status: "failed",
|
|
254
|
+
nextAttemptAt: null,
|
|
255
|
+
deliveryLeaseId: null,
|
|
256
|
+
deliveryLeaseExpiresAt: null,
|
|
257
|
+
failureCode: code,
|
|
258
|
+
failureMessage: message,
|
|
259
|
+
updatedAt: now,
|
|
260
|
+
})
|
|
261
|
+
.where(and(eq(workflowRunDispatch.anchorRunId, anchorRunId), inArray(workflowRunDispatch.status, ["pending", "acknowledged"])))
|
|
262
|
+
.returning({ id: workflowRunDispatch.id });
|
|
263
|
+
return rows.length;
|
|
264
|
+
},
|
|
265
|
+
async requeueUnsettled(anchorRunId) {
|
|
266
|
+
const rows = await db
|
|
267
|
+
.update(workflowRunDispatch)
|
|
268
|
+
.set({
|
|
269
|
+
status: "pending",
|
|
270
|
+
acknowledgedGeneration: null,
|
|
271
|
+
acknowledgedAt: null,
|
|
272
|
+
nextAttemptAt: sql `now()`,
|
|
273
|
+
deliveryLeaseId: null,
|
|
274
|
+
deliveryLeaseExpiresAt: null,
|
|
275
|
+
failureCode: null,
|
|
276
|
+
failureMessage: null,
|
|
277
|
+
updatedAt: sql `now()`,
|
|
278
|
+
})
|
|
279
|
+
.where(and(eq(workflowRunDispatch.anchorRunId, anchorRunId), inArray(workflowRunDispatch.status, ["pending", "acknowledged"])))
|
|
280
|
+
.returning({ id: workflowRunDispatch.id });
|
|
281
|
+
return rows.length;
|
|
282
|
+
},
|
|
283
|
+
async findById(id) {
|
|
284
|
+
const row = await db.query.workflowRunDispatch.findFirst({
|
|
285
|
+
where: eq(workflowRunDispatch.id, id),
|
|
286
|
+
});
|
|
287
|
+
return row === undefined ? null : parseWorkflowRunDispatchRow(row);
|
|
288
|
+
},
|
|
289
|
+
async listUnsettled(anchorRunId) {
|
|
290
|
+
const rows = await db
|
|
291
|
+
.select()
|
|
292
|
+
.from(workflowRunDispatch)
|
|
293
|
+
.where(and(eq(workflowRunDispatch.anchorRunId, anchorRunId), inArray(workflowRunDispatch.status, ["pending", "acknowledged"])))
|
|
294
|
+
.orderBy(asc(workflowRunDispatch.createdAt));
|
|
295
|
+
return rows.map(parseWorkflowRunDispatchRow);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|