@fungi.computer/braid 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/cloudflare/graph-engine.d.ts +25 -0
- package/dist/cloudflare/graph-engine.js +46 -0
- package/dist/cloudflare/graph-program.d.ts +6 -0
- package/dist/cloudflare/graph-program.js +550 -0
- package/dist/cloudflare/index.d.ts +1 -0
- package/dist/cloudflare/index.js +1 -0
- package/dist/code-execution.d.ts +16 -0
- package/dist/code-execution.js +50 -0
- package/dist/code-result.d.ts +11 -0
- package/dist/code-result.js +19 -0
- package/dist/code-sandbox.d.ts +9 -0
- package/dist/code-sandbox.js +13 -0
- package/dist/compile.d.ts +43 -0
- package/dist/compile.js +248 -0
- package/dist/execution.d.ts +194 -0
- package/dist/execution.js +130 -0
- package/dist/expressions.d.ts +16 -0
- package/dist/expressions.js +322 -0
- package/dist/git-access.d.ts +59 -0
- package/dist/git-access.js +106 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/ports.d.ts +58 -0
- package/dist/ports.js +256 -0
- package/dist/review-text.d.ts +15 -0
- package/dist/review-text.js +40 -0
- package/dist/runtime/arming-authority.d.ts +83 -0
- package/dist/runtime/arming-authority.js +90 -0
- package/dist/runtime/bindings.d.ts +25 -0
- package/dist/runtime/bindings.js +41 -0
- package/dist/runtime/capture.d.ts +18 -0
- package/dist/runtime/capture.js +99 -0
- package/dist/runtime/git-pass-contract.d.ts +63 -0
- package/dist/runtime/git-pass-contract.js +36 -0
- package/dist/runtime/git-pass-store.d.ts +32 -0
- package/dist/runtime/git-pass-store.js +82 -0
- package/dist/runtime/git-pass.d.ts +41 -0
- package/dist/runtime/git-pass.js +72 -0
- package/dist/runtime/graph-contract.d.ts +788 -0
- package/dist/runtime/graph-contract.js +122 -0
- package/dist/runtime/graph-dispatch.d.ts +31 -0
- package/dist/runtime/graph-dispatch.js +111 -0
- package/dist/runtime/graph-run-store.d.ts +79 -0
- package/dist/runtime/graph-run-store.js +267 -0
- package/dist/runtime/graph-start.d.ts +90 -0
- package/dist/runtime/graph-start.js +124 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +20 -0
- package/dist/runtime/ports.d.ts +156 -0
- package/dist/runtime/ports.js +1 -0
- package/dist/runtime/run-authority.d.ts +60 -0
- package/dist/runtime/run-authority.js +145 -0
- package/dist/runtime/run-ceiling.d.ts +106 -0
- package/dist/runtime/run-ceiling.js +23 -0
- package/dist/runtime/run-member.d.ts +3 -0
- package/dist/runtime/run-member.js +12 -0
- package/dist/runtime/run-owner.d.ts +1650 -0
- package/dist/runtime/run-owner.js +513 -0
- package/dist/runtime/run-report.d.ts +14 -0
- package/dist/runtime/run-report.js +23 -0
- package/dist/runtime/schedules.d.ts +39 -0
- package/dist/runtime/schedules.js +149 -0
- package/dist/runtime/stored-trigger-rows.d.ts +12 -0
- package/dist/runtime/stored-trigger-rows.js +17 -0
- package/dist/runtime/trigger-owner.d.ts +594 -0
- package/dist/runtime/trigger-owner.js +280 -0
- package/dist/runtime/trigger-plans.d.ts +774 -0
- package/dist/runtime/trigger-plans.js +62 -0
- package/dist/runtime/trigger-router.d.ts +34 -0
- package/dist/runtime/trigger-router.js +143 -0
- package/dist/runtime/trigger-run-reservations.d.ts +28 -0
- package/dist/runtime/trigger-run-reservations.js +114 -0
- package/dist/schema.d.ts +1183 -0
- package/dist/schema.js +239 -0
- package/dist/triggers.d.ts +46 -0
- package/dist/triggers.js +75 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.js +686 -0
- package/dist/workflow-id.d.ts +3 -0
- package/dist/workflow-id.js +3 -0
- package/package.json +96 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as S from "effect/Schema";
|
|
3
|
+
import { canonicalJson, executionPlanSchema, } from "../index.js";
|
|
4
|
+
import { armedWorkflowTriggerSchema, } from "./trigger-owner.js";
|
|
5
|
+
const rowSchema = S.Struct({
|
|
6
|
+
binding: S.String,
|
|
7
|
+
plan: S.String,
|
|
8
|
+
refusal: S.NullOr(S.String),
|
|
9
|
+
});
|
|
10
|
+
/** Immutable published plans survive Team owner recreation; only arm writes plans. */
|
|
11
|
+
export function createTriggerPlans(sql) {
|
|
12
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS hub_workflow_trigger_plans (
|
|
13
|
+
binding TEXT PRIMARY KEY, installation_id TEXT NOT NULL,
|
|
14
|
+
plan TEXT NOT NULL, refusal TEXT
|
|
15
|
+
) STRICT`);
|
|
16
|
+
const key = (binding) => canonicalJson(z.json().parse(armedWorkflowTriggerSchema.parse(binding)));
|
|
17
|
+
const decode = (raw) => {
|
|
18
|
+
const row = S.decodeUnknownSync(rowSchema)(raw);
|
|
19
|
+
return {
|
|
20
|
+
binding: armedWorkflowTriggerSchema.parse(JSON.parse(row.binding)),
|
|
21
|
+
plan: S.decodeUnknownSync(executionPlanSchema)(JSON.parse(row.plan), {
|
|
22
|
+
onExcessProperty: "error",
|
|
23
|
+
}),
|
|
24
|
+
refusal: row.refusal,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
put(binding, plan) {
|
|
29
|
+
sql.exec("INSERT INTO hub_workflow_trigger_plans(binding,installation_id,plan,refusal) VALUES(?,?,?,NULL) ON CONFLICT(binding) DO UPDATE SET plan=excluded.plan,refusal=NULL", key(binding), binding.installationId, JSON.stringify(S.decodeUnknownSync(executionPlanSchema)(plan)));
|
|
30
|
+
},
|
|
31
|
+
read(binding) {
|
|
32
|
+
const row = sql
|
|
33
|
+
.exec("SELECT binding,plan,refusal FROM hub_workflow_trigger_plans WHERE binding=?", key(binding))
|
|
34
|
+
.toArray()[0];
|
|
35
|
+
return row === undefined ? null : decode(row);
|
|
36
|
+
},
|
|
37
|
+
refusal(binding) {
|
|
38
|
+
const row = sql
|
|
39
|
+
.exec("SELECT refusal FROM hub_workflow_trigger_plans WHERE binding=?", key(binding))
|
|
40
|
+
.toArray()[0];
|
|
41
|
+
return row === undefined
|
|
42
|
+
? "Compiled Workflow unavailable; re-arm required"
|
|
43
|
+
: S.decodeUnknownSync(S.Struct({ refusal: S.NullOr(S.String) }))(row)
|
|
44
|
+
.refusal;
|
|
45
|
+
},
|
|
46
|
+
installation(installationId) {
|
|
47
|
+
return sql
|
|
48
|
+
.exec("SELECT binding,plan,refusal FROM hub_workflow_trigger_plans WHERE installation_id=?", installationId)
|
|
49
|
+
.toArray()
|
|
50
|
+
.map(decode);
|
|
51
|
+
},
|
|
52
|
+
refuse(binding, reason) {
|
|
53
|
+
sql.exec("UPDATE hub_workflow_trigger_plans SET refusal=? WHERE binding=?", reason, key(binding));
|
|
54
|
+
},
|
|
55
|
+
remove(binding) {
|
|
56
|
+
sql.exec("DELETE FROM hub_workflow_trigger_plans WHERE binding=?", key(binding));
|
|
57
|
+
},
|
|
58
|
+
invalidateInstallation(installationId) {
|
|
59
|
+
sql.exec("DELETE FROM hub_workflow_trigger_plans WHERE installation_id=?", installationId);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
import { type AppEventFact } from "@fungi.computer/puffball/app-event-facts";
|
|
3
|
+
import { type WorkflowArmer, type WorkflowInstallationAuthority, type WorkflowTriggerPrincipal, type WorkflowTriggerTarget, type createTeamWorkflowTriggers } from "./trigger-owner.js";
|
|
4
|
+
/** The Team's one App-event index; admission and execution belong to the shared trigger owner. */
|
|
5
|
+
export declare function createWorkflowEventRouter(options: {
|
|
6
|
+
sql: WatchdogSqliteOwner["sql"];
|
|
7
|
+
triggers: ReturnType<typeof createTeamWorkflowTriggers>;
|
|
8
|
+
teamId(): string;
|
|
9
|
+
commit<A>(body: () => A): Promise<A>;
|
|
10
|
+
authorize(memberId: string, installationId: string): Promise<void>;
|
|
11
|
+
}): {
|
|
12
|
+
arm(armer: WorkflowArmer, target: WorkflowTriggerTarget, computerId: string, installationAuthority: WorkflowInstallationAuthority): Promise<{
|
|
13
|
+
kind: "refused";
|
|
14
|
+
reason: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "armed";
|
|
17
|
+
eventTriggerId: string;
|
|
18
|
+
}>;
|
|
19
|
+
disarm(principal: WorkflowTriggerPrincipal, installationId: string, eventTriggerId: string): Promise<{
|
|
20
|
+
kind: "disarmed";
|
|
21
|
+
} | {
|
|
22
|
+
kind: "not_found";
|
|
23
|
+
}>;
|
|
24
|
+
list(principal: WorkflowTriggerPrincipal, installationId: string): Promise<{
|
|
25
|
+
eventTriggerId: string;
|
|
26
|
+
workflowId: string;
|
|
27
|
+
triggerId: string;
|
|
28
|
+
emitterInstallationId: string;
|
|
29
|
+
event: string;
|
|
30
|
+
state: "active" | "paused";
|
|
31
|
+
lastError: string | null;
|
|
32
|
+
}[]>;
|
|
33
|
+
deliver(raw: AppEventFact): Promise<"accepted">;
|
|
34
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canonicalJson, contentDigest } from "../index.js";
|
|
3
|
+
import { appEventFactSchema, } from "@fungi.computer/puffball/app-event-facts";
|
|
4
|
+
import { armedWorkflowTriggerSchema, workflowArmerKey, workflowTriggerControlledBy, workflowTriggerPrincipalMember, } from "./trigger-owner.js";
|
|
5
|
+
import { storedTriggerRows } from "./stored-trigger-rows.js";
|
|
6
|
+
const recordSchema = armedWorkflowTriggerSchema.extend({
|
|
7
|
+
id: z.string(),
|
|
8
|
+
emitterInstallationId: appEventFactSchema.shape.installationId,
|
|
9
|
+
appId: appEventFactSchema.shape.appId,
|
|
10
|
+
event: appEventFactSchema.shape.type,
|
|
11
|
+
state: z.enum(["active", "paused"]),
|
|
12
|
+
lastError: z.string().nullable(),
|
|
13
|
+
});
|
|
14
|
+
/** The Team's one App-event index; admission and execution belong to the shared trigger owner. */
|
|
15
|
+
export function createWorkflowEventRouter(options) {
|
|
16
|
+
const { sql } = options;
|
|
17
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS hub_workflow_event_triggers (
|
|
18
|
+
id TEXT PRIMARY KEY, record TEXT NOT NULL
|
|
19
|
+
) STRICT`);
|
|
20
|
+
sql.exec(`CREATE INDEX IF NOT EXISTS hub_workflow_event_trigger_source
|
|
21
|
+
ON hub_workflow_event_triggers(json_extract(record,'$.emitterInstallationId'),json_extract(record,'$.event'))`);
|
|
22
|
+
const rows = storedTriggerRows(sql, {
|
|
23
|
+
table: "hub_workflow_event_triggers",
|
|
24
|
+
key: "id",
|
|
25
|
+
record: recordSchema,
|
|
26
|
+
});
|
|
27
|
+
const readable = (raw) => raw.flatMap((row) => {
|
|
28
|
+
const decoded = rows.read(z.strictObject({ id: z.string() }).parse(row).id);
|
|
29
|
+
return decoded === null ? [] : [decoded];
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
async arm(armer, target, computerId, installationAuthority) {
|
|
33
|
+
await options.authorize(armer.armedBy, target.installationId);
|
|
34
|
+
const armed = await options.triggers.arm(armer, target, computerId, installationAuthority);
|
|
35
|
+
if (armed.kind === "refused")
|
|
36
|
+
return armed;
|
|
37
|
+
if (armed.trigger.on !== "event")
|
|
38
|
+
return {
|
|
39
|
+
kind: "refused",
|
|
40
|
+
reason: "Workflow trigger is not an App event",
|
|
41
|
+
};
|
|
42
|
+
const emitterInstallationId = armed.binding.bindings[armed.trigger.app];
|
|
43
|
+
const appId = armed.definition.apps[armed.trigger.app]?.appId;
|
|
44
|
+
if (emitterInstallationId === undefined || appId === undefined)
|
|
45
|
+
return {
|
|
46
|
+
kind: "refused",
|
|
47
|
+
reason: "Event App installation is not bound",
|
|
48
|
+
};
|
|
49
|
+
const id = await contentDigest([
|
|
50
|
+
"workflow-event",
|
|
51
|
+
...workflowArmerKey(armer),
|
|
52
|
+
target.installationId,
|
|
53
|
+
target.workflowId,
|
|
54
|
+
target.triggerId,
|
|
55
|
+
]);
|
|
56
|
+
const record = recordSchema.parse({
|
|
57
|
+
...armed.binding,
|
|
58
|
+
id,
|
|
59
|
+
emitterInstallationId,
|
|
60
|
+
appId,
|
|
61
|
+
event: armed.trigger.event,
|
|
62
|
+
state: "active",
|
|
63
|
+
lastError: null,
|
|
64
|
+
});
|
|
65
|
+
await options.commit(() => {
|
|
66
|
+
const previous = rows.read(id);
|
|
67
|
+
if (previous !== null) {
|
|
68
|
+
const binding = armedWorkflowTriggerSchema.strip().parse(previous);
|
|
69
|
+
if (canonicalJson(z.json().parse(binding)) !==
|
|
70
|
+
canonicalJson(z.json().parse(armed.binding)))
|
|
71
|
+
options.triggers.disarm(binding);
|
|
72
|
+
}
|
|
73
|
+
sql.exec("INSERT INTO hub_workflow_event_triggers(id,record) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET record=excluded.record", id, JSON.stringify(record));
|
|
74
|
+
});
|
|
75
|
+
return { kind: "armed", eventTriggerId: id };
|
|
76
|
+
},
|
|
77
|
+
async disarm(principal, installationId, eventTriggerId) {
|
|
78
|
+
const memberId = workflowTriggerPrincipalMember(principal);
|
|
79
|
+
await options.authorize(memberId, installationId);
|
|
80
|
+
const record = rows.read(eventTriggerId);
|
|
81
|
+
if (record === null)
|
|
82
|
+
return { kind: "not_found" };
|
|
83
|
+
if (record.installationId !== installationId ||
|
|
84
|
+
!workflowTriggerControlledBy(principal, record))
|
|
85
|
+
return { kind: "not_found" };
|
|
86
|
+
return options.commit(() => {
|
|
87
|
+
// Re-read inside the commit; a concurrent change may have replaced the row.
|
|
88
|
+
const current = rows.read(eventTriggerId);
|
|
89
|
+
if (current === null ||
|
|
90
|
+
!workflowTriggerControlledBy(principal, current))
|
|
91
|
+
return { kind: "not_found" };
|
|
92
|
+
options.triggers.disarm(armedWorkflowTriggerSchema.strip().parse(current));
|
|
93
|
+
rows.remove(eventTriggerId);
|
|
94
|
+
return { kind: "disarmed" };
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
async list(principal, installationId) {
|
|
98
|
+
await options.authorize(workflowTriggerPrincipalMember(principal), installationId);
|
|
99
|
+
const stored = readable(sql
|
|
100
|
+
.exec("SELECT id FROM hub_workflow_event_triggers WHERE json_valid(record) AND json_extract(record,'$.installationId')=? ORDER BY id", installationId)
|
|
101
|
+
.toArray()).map((record) => {
|
|
102
|
+
const refusal = options.triggers.refusal(armedWorkflowTriggerSchema.strip().parse(record));
|
|
103
|
+
return {
|
|
104
|
+
eventTriggerId: record.id,
|
|
105
|
+
workflowId: record.workflowId,
|
|
106
|
+
triggerId: record.triggerId,
|
|
107
|
+
emitterInstallationId: record.emitterInstallationId,
|
|
108
|
+
event: record.event,
|
|
109
|
+
state: refusal === null ? record.state : "paused",
|
|
110
|
+
lastError: record.lastError ?? refusal,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
return stored;
|
|
114
|
+
},
|
|
115
|
+
async deliver(raw) {
|
|
116
|
+
const fact = appEventFactSchema.parse(raw);
|
|
117
|
+
const records = readable(sql
|
|
118
|
+
.exec("SELECT id FROM hub_workflow_event_triggers WHERE json_valid(record) AND json_extract(record,'$.emitterInstallationId')=? AND json_extract(record,'$.event')=? ORDER BY id", fact.installationId, fact.type)
|
|
119
|
+
.toArray());
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
if (record.appId !== fact.appId || record.state !== "active")
|
|
122
|
+
continue;
|
|
123
|
+
const result = await options.triggers.fire(armedWorkflowTriggerSchema.strip().parse(record), canonicalJson([
|
|
124
|
+
options.teamId(),
|
|
125
|
+
"app-event",
|
|
126
|
+
record.id,
|
|
127
|
+
fact.installationId,
|
|
128
|
+
]), { payload: fact.payload, eventId: fact.eventId });
|
|
129
|
+
if (result.kind === "refused") {
|
|
130
|
+
await options.commit(() => {
|
|
131
|
+
// A concurrent disarm or re-arm owns the newer record.
|
|
132
|
+
sql.exec("UPDATE hub_workflow_event_triggers SET record=? WHERE id=? AND record=?", JSON.stringify({
|
|
133
|
+
...record,
|
|
134
|
+
state: "paused",
|
|
135
|
+
lastError: result.reason,
|
|
136
|
+
}), record.id, JSON.stringify(record));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return "accepted";
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
3
|
+
declare const triggerRunScopeSchema: z.ZodLazy<z.ZodObject<{
|
|
4
|
+
installationId: z.ZodString;
|
|
5
|
+
workflowId: z.ZodString;
|
|
6
|
+
commit: z.ZodString;
|
|
7
|
+
triggerId: z.ZodString;
|
|
8
|
+
}, z.core.$strict>>;
|
|
9
|
+
export type TriggerRunScope = z.output<typeof triggerRunScopeSchema>;
|
|
10
|
+
/** The enclosing Team owner supplies SQLite; callers never select the Team. */
|
|
11
|
+
export declare function createTriggerRunReservations(options: Pick<WatchdogSqliteOwner, "sql" | "transactionSync">): {
|
|
12
|
+
reserveTriggerRun(raw: {
|
|
13
|
+
scope: TriggerRunScope;
|
|
14
|
+
requestId: string;
|
|
15
|
+
}): {
|
|
16
|
+
kind: "reserved" | "replay";
|
|
17
|
+
reservationId: string;
|
|
18
|
+
runId: string | null;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "refused";
|
|
21
|
+
reason: "limit";
|
|
22
|
+
};
|
|
23
|
+
countActiveTriggerRuns(scope: TriggerRunScope): number;
|
|
24
|
+
releaseTriggerRun(rawReservationId: string): void;
|
|
25
|
+
bindTriggerRun(rawReservationId: string, rawRunId: string): "bound" | "replay" | "conflict";
|
|
26
|
+
terminalTriggerRun(rawRunId: string): void;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineIdFamily } from "@fungi.computer/cairn/id";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
|
|
4
|
+
import { workflowIdSchema } from "../index.js";
|
|
5
|
+
const reservationIds = defineIdFamily({
|
|
6
|
+
family: "workflow-trigger-reservation",
|
|
7
|
+
version: 1,
|
|
8
|
+
kinds: { reservation: { prefix: "wf_trigger_reservation" } },
|
|
9
|
+
});
|
|
10
|
+
const identity = z.string().min(1).max(256);
|
|
11
|
+
const triggerRunScopeSchema = z.lazy(() => z.strictObject({
|
|
12
|
+
installationId: identity,
|
|
13
|
+
workflowId: workflowIdSchema,
|
|
14
|
+
commit: gitSha1Schema,
|
|
15
|
+
triggerId: z.string().min(1).max(80),
|
|
16
|
+
}));
|
|
17
|
+
const reservationInputSchema = z.lazy(() => z.strictObject({
|
|
18
|
+
scope: triggerRunScopeSchema,
|
|
19
|
+
requestId: z.string().min(1).max(1024),
|
|
20
|
+
}));
|
|
21
|
+
const rowSchema = z.lazy(() => z.object({
|
|
22
|
+
reservation_id: identity,
|
|
23
|
+
run_id: identity.nullable(),
|
|
24
|
+
status: z.enum(["active", "released", "terminal"]),
|
|
25
|
+
}));
|
|
26
|
+
const countSchema = z.lazy(() => z.object({ count: z.number().int().nonnegative() }));
|
|
27
|
+
const scopeKey = (scope) => {
|
|
28
|
+
const parsed = triggerRunScopeSchema.parse(scope);
|
|
29
|
+
return JSON.stringify([
|
|
30
|
+
parsed.installationId,
|
|
31
|
+
parsed.workflowId,
|
|
32
|
+
parsed.commit,
|
|
33
|
+
parsed.triggerId,
|
|
34
|
+
]);
|
|
35
|
+
};
|
|
36
|
+
/** The enclosing Team owner supplies SQLite; callers never select the Team. */
|
|
37
|
+
export function createTriggerRunReservations(options) {
|
|
38
|
+
const { sql } = options;
|
|
39
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS hub_trigger_run_reservations (
|
|
40
|
+
reservation_id TEXT PRIMARY KEY, scope TEXT NOT NULL, request_id TEXT NOT NULL,
|
|
41
|
+
run_id TEXT UNIQUE, status TEXT NOT NULL CHECK(status IN ('active','released','terminal')),
|
|
42
|
+
UNIQUE(scope,request_id)
|
|
43
|
+
) STRICT`);
|
|
44
|
+
return {
|
|
45
|
+
reserveTriggerRun(raw) {
|
|
46
|
+
const input = reservationInputSchema.parse(raw);
|
|
47
|
+
const scope = scopeKey(input.scope);
|
|
48
|
+
return options.transactionSync(() => {
|
|
49
|
+
const stored = sql
|
|
50
|
+
.exec("SELECT reservation_id,run_id,status FROM hub_trigger_run_reservations WHERE scope=? AND request_id=?", scope, input.requestId)
|
|
51
|
+
.toArray()[0];
|
|
52
|
+
const prior = stored === undefined ? null : rowSchema.parse(stored);
|
|
53
|
+
if (prior !== null && prior.status !== "released")
|
|
54
|
+
return {
|
|
55
|
+
kind: "replay",
|
|
56
|
+
reservationId: prior.reservation_id,
|
|
57
|
+
runId: prior.run_id,
|
|
58
|
+
};
|
|
59
|
+
const active = countSchema.parse(sql
|
|
60
|
+
.exec("SELECT count(*) AS count FROM hub_trigger_run_reservations WHERE status='active'")
|
|
61
|
+
.toArray()[0]).count;
|
|
62
|
+
if (active >= 20)
|
|
63
|
+
return { kind: "refused", reason: "limit" };
|
|
64
|
+
const reservationId = prior?.reservation_id ?? reservationIds.reservation.generate();
|
|
65
|
+
if (prior === null)
|
|
66
|
+
sql.exec("INSERT INTO hub_trigger_run_reservations(reservation_id,scope,request_id,run_id,status) VALUES(?,?,?,NULL,'active')", reservationId, scope, input.requestId);
|
|
67
|
+
else
|
|
68
|
+
sql.exec("UPDATE hub_trigger_run_reservations SET status='active' WHERE reservation_id=?", reservationId);
|
|
69
|
+
return {
|
|
70
|
+
kind: "reserved",
|
|
71
|
+
reservationId,
|
|
72
|
+
runId: prior?.run_id ?? null,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
countActiveTriggerRuns(scope) {
|
|
77
|
+
return countSchema.parse(sql
|
|
78
|
+
.exec("SELECT count(*) AS count FROM hub_trigger_run_reservations WHERE scope=? AND status='active'", scopeKey(scope))
|
|
79
|
+
.toArray()[0]).count;
|
|
80
|
+
},
|
|
81
|
+
releaseTriggerRun(rawReservationId) {
|
|
82
|
+
const reservationId = identity.parse(rawReservationId);
|
|
83
|
+
options.transactionSync(() => {
|
|
84
|
+
sql.exec("UPDATE hub_trigger_run_reservations SET status='released' WHERE reservation_id=? AND status='active'", reservationId);
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
bindTriggerRun(rawReservationId, rawRunId) {
|
|
88
|
+
const reservationId = identity.parse(rawReservationId);
|
|
89
|
+
const runId = identity.parse(rawRunId);
|
|
90
|
+
return options.transactionSync(() => {
|
|
91
|
+
const stored = sql
|
|
92
|
+
.exec("SELECT reservation_id,run_id,status FROM hub_trigger_run_reservations WHERE reservation_id=?", reservationId)
|
|
93
|
+
.toArray()[0];
|
|
94
|
+
if (stored === undefined)
|
|
95
|
+
return "conflict";
|
|
96
|
+
const prior = rowSchema.parse(stored);
|
|
97
|
+
if (prior.run_id === runId)
|
|
98
|
+
return "replay";
|
|
99
|
+
if (prior.run_id !== null || prior.status !== "active")
|
|
100
|
+
return "conflict";
|
|
101
|
+
if (sql
|
|
102
|
+
.exec("SELECT reservation_id FROM hub_trigger_run_reservations WHERE run_id=?", runId)
|
|
103
|
+
.toArray().length > 0)
|
|
104
|
+
return "conflict";
|
|
105
|
+
sql.exec("UPDATE hub_trigger_run_reservations SET run_id=? WHERE reservation_id=?", runId, reservationId);
|
|
106
|
+
return "bound";
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
terminalTriggerRun(rawRunId) {
|
|
110
|
+
const runId = identity.parse(rawRunId);
|
|
111
|
+
sql.exec("UPDATE hub_trigger_run_reservations SET status='terminal' WHERE run_id=?", runId);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|