@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/dist/cloudflare/graph-engine.d.ts +25 -0
  4. package/dist/cloudflare/graph-engine.js +46 -0
  5. package/dist/cloudflare/graph-program.d.ts +6 -0
  6. package/dist/cloudflare/graph-program.js +550 -0
  7. package/dist/cloudflare/index.d.ts +1 -0
  8. package/dist/cloudflare/index.js +1 -0
  9. package/dist/code-execution.d.ts +16 -0
  10. package/dist/code-execution.js +50 -0
  11. package/dist/code-result.d.ts +11 -0
  12. package/dist/code-result.js +19 -0
  13. package/dist/code-sandbox.d.ts +9 -0
  14. package/dist/code-sandbox.js +13 -0
  15. package/dist/compile.d.ts +43 -0
  16. package/dist/compile.js +248 -0
  17. package/dist/execution.d.ts +194 -0
  18. package/dist/execution.js +130 -0
  19. package/dist/expressions.d.ts +16 -0
  20. package/dist/expressions.js +322 -0
  21. package/dist/git-access.d.ts +59 -0
  22. package/dist/git-access.js +106 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.js +9 -0
  25. package/dist/ports.d.ts +58 -0
  26. package/dist/ports.js +256 -0
  27. package/dist/review-text.d.ts +15 -0
  28. package/dist/review-text.js +40 -0
  29. package/dist/runtime/arming-authority.d.ts +83 -0
  30. package/dist/runtime/arming-authority.js +90 -0
  31. package/dist/runtime/bindings.d.ts +25 -0
  32. package/dist/runtime/bindings.js +41 -0
  33. package/dist/runtime/capture.d.ts +18 -0
  34. package/dist/runtime/capture.js +99 -0
  35. package/dist/runtime/git-pass-contract.d.ts +63 -0
  36. package/dist/runtime/git-pass-contract.js +36 -0
  37. package/dist/runtime/git-pass-store.d.ts +32 -0
  38. package/dist/runtime/git-pass-store.js +82 -0
  39. package/dist/runtime/git-pass.d.ts +41 -0
  40. package/dist/runtime/git-pass.js +72 -0
  41. package/dist/runtime/graph-contract.d.ts +788 -0
  42. package/dist/runtime/graph-contract.js +122 -0
  43. package/dist/runtime/graph-dispatch.d.ts +31 -0
  44. package/dist/runtime/graph-dispatch.js +111 -0
  45. package/dist/runtime/graph-run-store.d.ts +79 -0
  46. package/dist/runtime/graph-run-store.js +267 -0
  47. package/dist/runtime/graph-start.d.ts +90 -0
  48. package/dist/runtime/graph-start.js +124 -0
  49. package/dist/runtime/index.d.ts +20 -0
  50. package/dist/runtime/index.js +20 -0
  51. package/dist/runtime/ports.d.ts +156 -0
  52. package/dist/runtime/ports.js +1 -0
  53. package/dist/runtime/run-authority.d.ts +60 -0
  54. package/dist/runtime/run-authority.js +145 -0
  55. package/dist/runtime/run-ceiling.d.ts +106 -0
  56. package/dist/runtime/run-ceiling.js +23 -0
  57. package/dist/runtime/run-member.d.ts +3 -0
  58. package/dist/runtime/run-member.js +12 -0
  59. package/dist/runtime/run-owner.d.ts +1650 -0
  60. package/dist/runtime/run-owner.js +513 -0
  61. package/dist/runtime/run-report.d.ts +14 -0
  62. package/dist/runtime/run-report.js +23 -0
  63. package/dist/runtime/schedules.d.ts +39 -0
  64. package/dist/runtime/schedules.js +149 -0
  65. package/dist/runtime/stored-trigger-rows.d.ts +12 -0
  66. package/dist/runtime/stored-trigger-rows.js +17 -0
  67. package/dist/runtime/trigger-owner.d.ts +594 -0
  68. package/dist/runtime/trigger-owner.js +280 -0
  69. package/dist/runtime/trigger-plans.d.ts +774 -0
  70. package/dist/runtime/trigger-plans.js +62 -0
  71. package/dist/runtime/trigger-router.d.ts +34 -0
  72. package/dist/runtime/trigger-router.js +143 -0
  73. package/dist/runtime/trigger-run-reservations.d.ts +28 -0
  74. package/dist/runtime/trigger-run-reservations.js +114 -0
  75. package/dist/schema.d.ts +1183 -0
  76. package/dist/schema.js +239 -0
  77. package/dist/triggers.d.ts +46 -0
  78. package/dist/triggers.js +75 -0
  79. package/dist/validate.d.ts +25 -0
  80. package/dist/validate.js +686 -0
  81. package/dist/workflow-id.d.ts +3 -0
  82. package/dist/workflow-id.js +3 -0
  83. package/package.json +96 -3
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ import * as Result from "effect/Result";
3
+ import { afterAdmitted, afterRefused, isDue, nextOccurrence, occurrenceKey, paused, cancelled, } from "@fungi.computer/almanac";
4
+ import { canonicalJson, contentDigest } from "../index.js";
5
+ import { armedWorkflowTriggerSchema, workflowArmerKey, workflowTriggerControlledBy, workflowTriggerPrincipalMember, } from "./trigger-owner.js";
6
+ import { storedTriggerRows } from "./stored-trigger-rows.js";
7
+ const recordSchema = armedWorkflowTriggerSchema.extend({
8
+ id: z.string().min(1).max(256),
9
+ cron: z.strictObject({
10
+ expression: z.string().max(256),
11
+ timeZone: z.string().max(128),
12
+ }),
13
+ nextDueAt: z.number().int(),
14
+ state: z.enum(["active", "paused", "cancelled", "finished"]),
15
+ lastError: z.string().nullable(),
16
+ attempt: z.number().int().nonnegative(),
17
+ retryAt: z.number().int().nullable(),
18
+ });
19
+ const bindingOf = (record) => armedWorkflowTriggerSchema.strip().parse(record);
20
+ /** Team-owned schedule records; Almanac alone decides occurrences and transitions. */
21
+ export function createWorkflowSchedules(options) {
22
+ const { sql, reportFailure } = options;
23
+ sql.exec(`CREATE TABLE IF NOT EXISTS hub_workflow_schedules (scheduleId TEXT PRIMARY KEY, record TEXT NOT NULL) STRICT`);
24
+ const rows = storedTriggerRows(sql, {
25
+ table: "hub_workflow_schedules",
26
+ key: "scheduleId",
27
+ record: recordSchema,
28
+ });
29
+ const records = () => rows.all().map((record) => {
30
+ if (record.state !== "active")
31
+ return record;
32
+ const reason = options.triggers.refusal(bindingOf(record));
33
+ if (reason === null)
34
+ return record;
35
+ const next = { ...paused(record), lastError: reason, retryAt: null };
36
+ store(next);
37
+ return next;
38
+ });
39
+ const nextWake = () => {
40
+ const due = records()
41
+ .filter((record) => record.state === "active")
42
+ .map((record) => Math.max(record.nextDueAt, record.retryAt ?? record.nextDueAt));
43
+ return due.length === 0 ? null : Math.min(...due);
44
+ };
45
+ const store = (record) => {
46
+ const checked = recordSchema.parse(record);
47
+ sql.exec("INSERT INTO hub_workflow_schedules(scheduleId,record) VALUES(?,?) ON CONFLICT(scheduleId) DO UPDATE SET record=excluded.record", checked.id, JSON.stringify(checked));
48
+ };
49
+ return {
50
+ nextWake,
51
+ async arm(armer, target, computerId, installationAuthority) {
52
+ await options.authorize(armer.armedBy, target.installationId);
53
+ const armed = await options.triggers.arm(armer, target, computerId, installationAuthority);
54
+ if (armed.kind === "refused")
55
+ return armed;
56
+ if (armed.trigger.on !== "schedule")
57
+ return {
58
+ kind: "refused",
59
+ reason: "Workflow trigger is not a schedule",
60
+ };
61
+ const cron = armed.trigger.cron;
62
+ const next = nextOccurrence(cron, options.now());
63
+ if (Result.isFailure(next))
64
+ return { kind: "refused", reason: next.failure.message };
65
+ const id = await contentDigest([
66
+ "workflow-schedule",
67
+ ...workflowArmerKey(armer),
68
+ target.installationId,
69
+ target.workflowId,
70
+ target.triggerId,
71
+ ]);
72
+ await options.commit(() => {
73
+ const previous = records().find((record) => record.id === id);
74
+ if (previous !== undefined &&
75
+ canonicalJson(z.json().parse(bindingOf(previous))) !==
76
+ canonicalJson(z.json().parse(armed.binding)))
77
+ options.triggers.disarm(bindingOf(previous));
78
+ store({
79
+ ...armed.binding,
80
+ id,
81
+ cron,
82
+ nextDueAt: next.success,
83
+ state: "active",
84
+ lastError: null,
85
+ attempt: 0,
86
+ retryAt: null,
87
+ });
88
+ });
89
+ await options.wake(nextWake());
90
+ return { kind: "armed", scheduleId: id };
91
+ },
92
+ async list(principal, installationId) {
93
+ await options.authorize(workflowTriggerPrincipalMember(principal), installationId);
94
+ return records()
95
+ .filter((record) => record.installationId === installationId)
96
+ .map((record) => ({
97
+ scheduleId: record.id,
98
+ workflowId: record.workflowId,
99
+ triggerId: record.triggerId,
100
+ nextDueAt: record.nextDueAt,
101
+ state: record.state,
102
+ lastError: record.lastError,
103
+ }));
104
+ },
105
+ async disarm(principal, installationId, scheduleId) {
106
+ const memberId = workflowTriggerPrincipalMember(principal);
107
+ await options.authorize(memberId, installationId);
108
+ const record = rows.read(scheduleId);
109
+ if (record === null)
110
+ return { kind: "not_found" };
111
+ if (record.installationId !== installationId ||
112
+ !workflowTriggerControlledBy(principal, record))
113
+ return { kind: "not_found" };
114
+ await options.commit(() => {
115
+ store(cancelled(record));
116
+ options.triggers.disarm(bindingOf(record));
117
+ });
118
+ await options.wake(nextWake());
119
+ return { kind: "disarmed" };
120
+ },
121
+ async fireDue() {
122
+ for (const record of records()) {
123
+ if (!isDue(record, options.now()) ||
124
+ (record.retryAt !== null && record.retryAt > options.now()))
125
+ continue;
126
+ const binding = bindingOf(record);
127
+ let next;
128
+ try {
129
+ const result = await options.triggers.fire(binding, `${options.teamId()}/schedule/${record.id}`, {
130
+ scheduledAt: new Date(record.nextDueAt).toISOString(),
131
+ eventId: occurrenceKey(record),
132
+ });
133
+ next =
134
+ result.kind === "refused"
135
+ ? { ...paused(record), lastError: result.reason, retryAt: null }
136
+ : { ...afterAdmitted(record, options.now()), retryAt: null };
137
+ }
138
+ catch (cause) {
139
+ reportFailure({ source: "workflow_schedule_fire", cause });
140
+ const refused = afterRefused(record, options.now(), "Workflow execution temporarily unavailable");
141
+ next = { ...refused.schedule, retryAt: refused.retryAt };
142
+ }
143
+ // A disarm or re-arm admitted during upstream I/O wins over this firing.
144
+ sql.exec("UPDATE hub_workflow_schedules SET record=? WHERE scheduleId=? AND record=?", JSON.stringify(recordSchema.parse(next)), record.id, JSON.stringify(record));
145
+ }
146
+ await options.wake(nextWake());
147
+ },
148
+ };
149
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
3
+ /** Stored trigger rows decode strictly; an undecodable row is corruption. */
4
+ export declare function storedTriggerRows<Record extends z.ZodType>(sql: WatchdogSqliteOwner["sql"], table: Readonly<{
5
+ table: "hub_workflow_schedules" | "hub_workflow_event_triggers";
6
+ key: "scheduleId" | "id";
7
+ record: Record;
8
+ }>): {
9
+ all: () => z.core.output<Record>[];
10
+ read(id: string): z.core.output<Record> | null;
11
+ remove(id: string): void;
12
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ const rowSchema = z.strictObject({ id: z.string(), record: z.string() });
3
+ /** Stored trigger rows decode strictly; an undecodable row is corruption. */
4
+ export function storedTriggerRows(sql, table) {
5
+ const select = `SELECT ${table.key} AS id, record FROM ${table.table}`;
6
+ const decode = (raw) => table.record.parse(JSON.parse(rowSchema.parse(raw).record));
7
+ return {
8
+ all: () => sql.exec(`${select} ORDER BY ${table.key}`).toArray().map(decode),
9
+ read(id) {
10
+ const row = sql.exec(`${select} WHERE ${table.key}=?`, id).toArray()[0];
11
+ return row === undefined ? null : decode(row);
12
+ },
13
+ remove(id) {
14
+ sql.exec(`DELETE FROM ${table.table} WHERE ${table.key}=?`, id);
15
+ },
16
+ };
17
+ }