@sanoma-ai/workflows 0.5.0 → 0.7.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 (153) hide show
  1. package/README.md +49 -15
  2. package/dist/apply.d.ts +2 -1
  3. package/dist/apply.js +29 -7
  4. package/dist/apply.js.map +1 -1
  5. package/dist/approvals.d.ts +27 -2
  6. package/dist/approvals.js +73 -9
  7. package/dist/approvals.js.map +1 -1
  8. package/dist/call.d.ts +3 -2
  9. package/dist/call.js +38 -30
  10. package/dist/call.js.map +1 -1
  11. package/dist/cedar.js +24 -9
  12. package/dist/cedar.js.map +1 -1
  13. package/dist/change.d.ts +219 -0
  14. package/dist/change.js +150 -0
  15. package/dist/change.js.map +1 -0
  16. package/dist/chats.d.ts +43 -0
  17. package/dist/chats.js +48 -0
  18. package/dist/chats.js.map +1 -0
  19. package/dist/client.d.ts +52 -6
  20. package/dist/client.js +88 -47
  21. package/dist/client.js.map +1 -1
  22. package/dist/config.d.ts +47 -5
  23. package/dist/config.js +71 -7
  24. package/dist/config.js.map +1 -1
  25. package/dist/database.d.ts +83 -0
  26. package/dist/database.js +92 -0
  27. package/dist/database.js.map +1 -0
  28. package/dist/db.d.ts +31 -1
  29. package/dist/db.js +89 -7
  30. package/dist/db.js.map +1 -1
  31. package/dist/deadlines.d.ts +88 -0
  32. package/dist/deadlines.js +171 -0
  33. package/dist/deadlines.js.map +1 -0
  34. package/dist/define.d.ts +25 -4
  35. package/dist/define.js +40 -0
  36. package/dist/define.js.map +1 -1
  37. package/dist/demo.d.ts +28 -0
  38. package/dist/demo.js +72 -0
  39. package/dist/demo.js.map +1 -0
  40. package/dist/describe.d.ts +6 -3
  41. package/dist/describe.js +11 -7
  42. package/dist/describe.js.map +1 -1
  43. package/dist/errors.d.ts +10 -1
  44. package/dist/errors.js +13 -0
  45. package/dist/errors.js.map +1 -1
  46. package/dist/fake.d.ts +8 -0
  47. package/dist/fake.js +11 -1
  48. package/dist/fake.js.map +1 -1
  49. package/dist/grid.d.ts +34 -0
  50. package/dist/grid.js +62 -0
  51. package/dist/grid.js.map +1 -0
  52. package/dist/host.d.ts +13 -2
  53. package/dist/host.js +2 -2
  54. package/dist/host.js.map +1 -1
  55. package/dist/index.d.ts +16 -8
  56. package/dist/index.js +12 -5
  57. package/dist/index.js.map +1 -1
  58. package/dist/ledger.d.ts +11 -2
  59. package/dist/ledger.js.map +1 -1
  60. package/dist/lint.js +1 -1
  61. package/dist/lint.js.map +1 -1
  62. package/dist/op.d.ts +14 -2
  63. package/dist/op.js.map +1 -1
  64. package/dist/policy.d.ts +7 -1
  65. package/dist/policy.js +2 -1
  66. package/dist/policy.js.map +1 -1
  67. package/dist/resource.d.ts +13 -1
  68. package/dist/resource.js +9 -1
  69. package/dist/resource.js.map +1 -1
  70. package/dist/resources.d.ts +2 -0
  71. package/dist/resources.js +10 -1
  72. package/dist/resources.js.map +1 -1
  73. package/dist/run.d.ts +5 -0
  74. package/dist/run.js.map +1 -1
  75. package/dist/sandbox.d.ts +38 -10
  76. package/dist/sandbox.js +66 -35
  77. package/dist/sandbox.js.map +1 -1
  78. package/dist/scenario.d.ts +18 -1
  79. package/dist/scenario.js +77 -27
  80. package/dist/scenario.js.map +1 -1
  81. package/dist/scenarios.d.ts +10 -1
  82. package/dist/scenarios.js +19 -4
  83. package/dist/scenarios.js.map +1 -1
  84. package/dist/schedules.d.ts +66 -0
  85. package/dist/schedules.js +158 -0
  86. package/dist/schedules.js.map +1 -0
  87. package/dist/schema.d.ts +6 -0
  88. package/dist/schema.js +13 -3
  89. package/dist/schema.js.map +1 -1
  90. package/dist/shared.d.ts +38 -2
  91. package/dist/shared.js +50 -4
  92. package/dist/shared.js.map +1 -1
  93. package/dist/sites.d.ts +38 -1
  94. package/dist/sites.js +153 -26
  95. package/dist/sites.js.map +1 -1
  96. package/dist/source.d.ts +61 -2
  97. package/dist/source.js.map +1 -1
  98. package/dist/supervisor.d.ts +38 -0
  99. package/dist/supervisor.js +77 -0
  100. package/dist/supervisor.js.map +1 -0
  101. package/dist/version.d.ts +3 -1
  102. package/dist/version.js +3 -1
  103. package/dist/version.js.map +1 -1
  104. package/dist/wakeup-service.d.ts +83 -0
  105. package/dist/wakeup-service.js +421 -0
  106. package/dist/wakeup-service.js.map +1 -0
  107. package/dist/wakeup.d.ts +84 -0
  108. package/dist/wakeup.js +87 -0
  109. package/dist/wakeup.js.map +1 -0
  110. package/dist/worker.d.ts +39 -1
  111. package/dist/worker.js +130 -39
  112. package/dist/worker.js.map +1 -1
  113. package/oxlint.json +2 -1
  114. package/package.json +8 -2
  115. package/src/apply.ts +25 -6
  116. package/src/approvals.ts +89 -16
  117. package/src/call.ts +40 -29
  118. package/src/cedar.ts +24 -6
  119. package/src/change.ts +222 -0
  120. package/src/chats.ts +114 -0
  121. package/src/client.ts +145 -55
  122. package/src/config.ts +110 -13
  123. package/src/database.ts +152 -0
  124. package/src/db.ts +108 -9
  125. package/src/deadlines.ts +234 -0
  126. package/src/define.ts +71 -4
  127. package/src/demo.ts +87 -0
  128. package/src/describe.ts +19 -7
  129. package/src/errors.ts +14 -0
  130. package/src/fake.ts +12 -0
  131. package/src/grid.ts +100 -0
  132. package/src/host.ts +13 -4
  133. package/src/index.ts +62 -3
  134. package/src/ledger.ts +15 -3
  135. package/src/lint.ts +1 -1
  136. package/src/op.ts +15 -2
  137. package/src/policy.ts +7 -1
  138. package/src/resource.ts +22 -3
  139. package/src/resources.ts +12 -1
  140. package/src/run.ts +5 -0
  141. package/src/sandbox.ts +97 -37
  142. package/src/scenario.ts +97 -28
  143. package/src/scenarios.ts +21 -5
  144. package/src/schedules.ts +206 -0
  145. package/src/schema.ts +16 -3
  146. package/src/shared.ts +62 -4
  147. package/src/sites.ts +198 -33
  148. package/src/source.ts +58 -3
  149. package/src/supervisor.ts +107 -0
  150. package/src/version.ts +3 -1
  151. package/src/wakeup-service.ts +511 -0
  152. package/src/wakeup.ts +150 -0
  153. package/src/worker.ts +181 -44
@@ -0,0 +1,83 @@
1
+ /** Where the accounts live: one per party that registers instances, its token's hash alone. */
2
+ export declare const WAKEUP_ACCOUNTS_TABLE = "sanoma_wakeup_accounts";
3
+ /** Where the instances live: each one's URL, its token's hash and its registered wake. */
4
+ export declare const WAKEUP_INSTANCES_TABLE = "sanoma_wakeup_instances";
5
+ /** The shape of `@sanoma-ai/app`'s `IngressRoute`, which `startApp`'s `ingress` takes. */
6
+ export interface WakeupRoute {
7
+ method?: "GET" | "POST" | "PUT" | "DELETE";
8
+ /** `/wakeup`, with every path under it (`prefix`). */
9
+ path: string;
10
+ prefix: true;
11
+ handle(request: Request): Promise<Response>;
12
+ }
13
+ export interface WakeupServiceOptions {
14
+ /** The service's own Postgres: its tables are made there when missing (the database too). */
15
+ databaseUrl: string;
16
+ /** The clock: what is due, and what `called_at` and `next_attempt_at` are set from. */
17
+ now?: () => Date;
18
+ /** How the instances are called. */
19
+ fetch?: typeof globalThis.fetch;
20
+ /** One `info` line per call, a `warn` per failure; the runtime's logger by default. */
21
+ log?: {
22
+ info(line: string): void;
23
+ warn(line: string): void;
24
+ };
25
+ }
26
+ export interface WakeupService {
27
+ /** One route answering everything under `/wakeup`, for `startApp`'s `ingress` or a server of its own. */
28
+ routes: WakeupRoute[];
29
+ /** An account that may register instances: its id, and its token, shown this once. */
30
+ mintAccount(label: string): Promise<{
31
+ id: string;
32
+ token: string;
33
+ }>;
34
+ /** Refuses the account's token and its instances' from now on, and wakes its instances no more. */
35
+ revokeAccount(id: string): Promise<void>;
36
+ /** Ends the loop, abandoning the calls in flight (called again once their claim lapses), and closes the pool. */
37
+ stop(): Promise<void>;
38
+ }
39
+ /**
40
+ * A new instance token (`swi_…`), which the company mints and stores as `SANOMA_WAKEUP_TOKEN`
41
+ * before anything is applied, so its host is given it from the first apply; the wakeup's create
42
+ * then registers it with the service. The service never mints one.
43
+ */
44
+ export declare const mintInstanceToken: () => string;
45
+ /** The secret a company keeps its instance token in: what its host's `secrets` name and its wakeup's driver reads. */
46
+ export declare const WAKEUP_TOKEN = "SANOMA_WAKEUP_TOKEN";
47
+ /** An instance token the service takes: `swi_`, then at least as many base64url characters as a minted one has. */
48
+ export declare const INSTANCE_TOKEN: RegExp;
49
+ /**
50
+ * An instance's URL as it is stored, without a trailing slash, or why it is refused: `http(s)`, no
51
+ * user or password, no query, no fragment (`/wake?generation=N` is added to it).
52
+ */
53
+ export declare function instanceUrl(value: unknown): {
54
+ url: string;
55
+ } | {
56
+ error: string;
57
+ };
58
+ /**
59
+ * An account of the wakeup service on `databaseUrl`, minted without starting the service (its
60
+ * tables made when missing): what `sanoma wakeup account <label>` prints.
61
+ */
62
+ export declare function mintWakeupAccount(databaseUrl: string, label: string): Promise<{
63
+ id: string;
64
+ token: string;
65
+ }>;
66
+ /**
67
+ * Starts the wakeup service on its own database: its two tables made when missing, its one loop
68
+ * running, and `routes` for whoever serves it (`sanoma wakeup`, or `sanoma serve` with
69
+ * `wakeup: true`). Routes, each JSON and `no-store`:
70
+ *
71
+ * - `POST /wakeup/instances { url, token }`, an account's token → 201 `{ id }` for a new instance
72
+ * token, 200 `{ id }` for one the account registered already, its URL now the given one (a lost
73
+ * reply, or a host that moved; its registered wake kept only when the URL is the same), 409 for
74
+ * another account's
75
+ * - `GET /wakeup/instances/{id}`, its account's token → `{ id, url, at, generation, calledAt, attempts, lastError }`
76
+ * - `DELETE /wakeup/instances/{id}`, its account's token → 204
77
+ * - `PUT /wakeup/wake { at, generation }`, an instance's own token, which names the instance → 204,
78
+ * applied only when `generation` is newer than the one it has, and waking the loop
79
+ *
80
+ * 401 without a token or with an unknown one, 403 for another account's instance, 400 for a bad
81
+ * body, 404 for an unknown id or path.
82
+ */
83
+ export declare function startWakeupService({ databaseUrl, now, fetch, log, }: WakeupServiceOptions): Promise<WakeupService>;
@@ -0,0 +1,421 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { connectMaking, createTable, openPool } from "./db.js";
3
+ import { info, warn } from "./log.js";
4
+ import { generateSecret } from "./secret.js";
5
+ import { errorMessage } from "./shared.js";
6
+ /*
7
+ * `@sanoma-ai/workflows/wakeup-service`: the service that wakes stopped instances, server-side
8
+ * only. An account (minted by whoever runs the service) registers an instance by its URL and the
9
+ * instance's own token, which the company mints (`mintInstanceToken`) and keeps; the instance,
10
+ * before it stops, `PUT`s with that token when it wants waking and a
11
+ * generation; the service then calls `GET {url}/wake?generation=N`, retrying until an answer is a
12
+ * 2xx. It makes its two tables itself, in the database it is given (sanoma-ops's own, when it
13
+ * hosts it): never among `db.ts`'s, which every company's database gets.
14
+ */
15
+ /** Where the accounts live: one per party that registers instances, its token's hash alone. */
16
+ export const WAKEUP_ACCOUNTS_TABLE = "sanoma_wakeup_accounts";
17
+ /** Where the instances live: each one's URL, its token's hash and its registered wake. */
18
+ export const WAKEUP_INSTANCES_TABLE = "sanoma_wakeup_instances";
19
+ const TABLES = [
20
+ [
21
+ "wakeup accounts",
22
+ `CREATE TABLE IF NOT EXISTS ${WAKEUP_ACCOUNTS_TABLE} (id uuid PRIMARY KEY, token_hash text UNIQUE NOT NULL, ` +
23
+ "label text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), revoked_at timestamptz)",
24
+ ],
25
+ [
26
+ "wakeup instances",
27
+ `CREATE TABLE IF NOT EXISTS ${WAKEUP_INSTANCES_TABLE} (id uuid PRIMARY KEY, ` +
28
+ `account_id uuid NOT NULL REFERENCES ${WAKEUP_ACCOUNTS_TABLE} (id), url text NOT NULL, ` +
29
+ "token_hash text UNIQUE NOT NULL, at timestamptz, generation bigint NOT NULL DEFAULT 0, " +
30
+ "called_at timestamptz, attempts integer NOT NULL DEFAULT 0, next_attempt_at timestamptz, " +
31
+ "last_error text, created_at timestamptz NOT NULL DEFAULT now())",
32
+ ],
33
+ // Each on its own, so one another process made first does not skip the next.
34
+ [
35
+ "wakeup instances",
36
+ `CREATE INDEX IF NOT EXISTS ${WAKEUP_INSTANCES_TABLE}_at ON ${WAKEUP_INSTANCES_TABLE} (at) WHERE at IS NOT NULL`,
37
+ ],
38
+ [
39
+ "wakeup instances",
40
+ `CREATE INDEX IF NOT EXISTS ${WAKEUP_INSTANCES_TABLE}_account ON ${WAKEUP_INSTANCES_TABLE} (account_id)`,
41
+ ],
42
+ ];
43
+ /** How long a call to an instance's `/wake` may take. */
44
+ const CALL_TIMEOUT_MS = 10_000;
45
+ /** The longest the loop sleeps without looking again, whatever is due. */
46
+ const MAX_SLEEP_MS = 60_000;
47
+ /** How many instances are called at once. */
48
+ const CONCURRENCY = 8;
49
+ /** The longest wait between two tries of a failing instance, in seconds. */
50
+ const MAX_BACKOFF_S = 300;
51
+ /**
52
+ * How long a row taken for a call is the taker's: longer than a call may take. Two services on one
53
+ * database never call one row at once, and a call no outcome was recorded for (the process
54
+ * stopped, or the write failed) is tried again once it lapses.
55
+ */
56
+ const CLAIM_S = 30;
57
+ /** How many due rows a round takes: few enough to be called well within their claim. */
58
+ const ROUND_ROWS = CONCURRENCY * 2;
59
+ /** The most a request's body may hold: a registration is a few hundred bytes. */
60
+ const MAX_BODY = 64 * 1024;
61
+ /** A new token: the prefix, then 32 random bytes in base64url. */
62
+ const newToken = (prefix) => `${prefix}${generateSecret()}`;
63
+ /**
64
+ * A new instance token (`swi_…`), which the company mints and stores as `SANOMA_WAKEUP_TOKEN`
65
+ * before anything is applied, so its host is given it from the first apply; the wakeup's create
66
+ * then registers it with the service. The service never mints one.
67
+ */
68
+ export const mintInstanceToken = () => newToken("swi_");
69
+ /** The secret a company keeps its instance token in: what its host's `secrets` name and its wakeup's driver reads. */
70
+ export const WAKEUP_TOKEN = "SANOMA_WAKEUP_TOKEN";
71
+ /** An instance token the service takes: `swi_`, then at least as many base64url characters as a minted one has. */
72
+ export const INSTANCE_TOKEN = /^swi_[\w-]{43,256}$/;
73
+ /**
74
+ * The token's sha256, which is all the tables keep: a token is found by its hash (a unique index),
75
+ * so the secret itself is never compared, and a guess's timing says nothing an attacker can steer.
76
+ */
77
+ const hashOf = (token) => createHash("sha256").update(token).digest("hex");
78
+ /** A date-time as `Date.toISOString` writes one, or with an offset: four-digit years only. */
79
+ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
80
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
81
+ const HEADERS = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
82
+ const json = (status, body) => Response.json(body, { status, headers: HEADERS });
83
+ const problem = (status, error, headers = {}) => Response.json({ error }, { status, headers: { ...HEADERS, ...headers } });
84
+ /** 405, naming the methods the path answers. */
85
+ const notAllowed = (allow) => problem(405, `this path answers ${allow} only`, { allow });
86
+ const noContent = () => new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
87
+ /** The request's bearer token, or undefined without one. */
88
+ function bearer(request) {
89
+ const match = /^Bearer (\S+)$/i.exec(request.headers.get("authorization") ?? "");
90
+ return match?.[1];
91
+ }
92
+ /** The request's body as text, or undefined once it holds more than `MAX_BODY` bytes. */
93
+ async function bodyText(request) {
94
+ if (!request.body)
95
+ return "";
96
+ const reader = request.body.getReader();
97
+ const chunks = [];
98
+ let size = 0;
99
+ for (let read = await reader.read(); !read.done; read = await reader.read()) {
100
+ size += read.value.length;
101
+ if (size > MAX_BODY) {
102
+ await reader.cancel();
103
+ return undefined;
104
+ }
105
+ chunks.push(read.value);
106
+ }
107
+ return Buffer.concat(chunks).toString("utf8");
108
+ }
109
+ /** The request's JSON object, undefined when its body is not one, or a 413 for one too large. */
110
+ async function jsonBody(request) {
111
+ const text = await bodyText(request);
112
+ if (text === undefined)
113
+ return problem(413, "the body is too large");
114
+ try {
115
+ const parsed = JSON.parse(text);
116
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
117
+ ? parsed
118
+ : undefined;
119
+ }
120
+ catch {
121
+ return undefined;
122
+ }
123
+ }
124
+ /**
125
+ * An instance's URL as it is stored, without a trailing slash, or why it is refused: `http(s)`, no
126
+ * user or password, no query, no fragment (`/wake?generation=N` is added to it).
127
+ */
128
+ export function instanceUrl(value) {
129
+ const refused = { error: "url must be an http(s) URL with no user, password, query or fragment" };
130
+ if (typeof value !== "string" || /[?#]/.test(value) || !URL.canParse(value))
131
+ return refused;
132
+ const url = new URL(value);
133
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password)
134
+ return refused;
135
+ return { url: url.href.replace(/\/+$/, "") };
136
+ }
137
+ /** An instance as `GET` shows it to its account. */
138
+ const shownInstance = (row) => ({
139
+ id: row.id,
140
+ url: row.url,
141
+ at: row.at?.toISOString() ?? null,
142
+ generation: Number(row.generation),
143
+ calledAt: row.called_at?.toISOString() ?? null,
144
+ attempts: row.attempts,
145
+ lastError: row.last_error,
146
+ });
147
+ /** A row waiting for its call: registered, not called since its `at`. */
148
+ const WAITING = "i.at IS NOT NULL AND (i.called_at IS NULL OR i.called_at < i.at)";
149
+ /** Of an account not revoked. */
150
+ const LIVE = `JOIN ${WAKEUP_ACCOUNTS_TABLE} a ON a.id = i.account_id AND a.revoked_at IS NULL`;
151
+ /**
152
+ * Runs `use` on a connection to `databaseUrl` once the service's two tables are there: the
153
+ * database and the tables made when missing.
154
+ */
155
+ async function withTables(databaseUrl, use) {
156
+ const conn = await connectMaking(databaseUrl);
157
+ try {
158
+ for (const [what, ddl] of TABLES)
159
+ await createTable(conn, ddl, what);
160
+ return await use(conn);
161
+ }
162
+ finally {
163
+ await conn.end().catch(() => undefined);
164
+ }
165
+ }
166
+ async function insertAccount(db, label) {
167
+ if (!label.trim())
168
+ throw new Error("An account's label must not be empty");
169
+ const id = randomUUID();
170
+ const token = newToken("swa_");
171
+ await db.query(`INSERT INTO ${WAKEUP_ACCOUNTS_TABLE} (id, token_hash, label) VALUES ($1, $2, $3)`, [
172
+ id,
173
+ hashOf(token),
174
+ label,
175
+ ]);
176
+ return { id, token };
177
+ }
178
+ /**
179
+ * An account of the wakeup service on `databaseUrl`, minted without starting the service (its
180
+ * tables made when missing): what `sanoma wakeup account <label>` prints.
181
+ */
182
+ export async function mintWakeupAccount(databaseUrl, label) {
183
+ return withTables(databaseUrl, (db) => insertAccount(db, label));
184
+ }
185
+ /**
186
+ * Starts the wakeup service on its own database: its two tables made when missing, its one loop
187
+ * running, and `routes` for whoever serves it (`sanoma wakeup`, or `sanoma serve` with
188
+ * `wakeup: true`). Routes, each JSON and `no-store`:
189
+ *
190
+ * - `POST /wakeup/instances { url, token }`, an account's token → 201 `{ id }` for a new instance
191
+ * token, 200 `{ id }` for one the account registered already, its URL now the given one (a lost
192
+ * reply, or a host that moved; its registered wake kept only when the URL is the same), 409 for
193
+ * another account's
194
+ * - `GET /wakeup/instances/{id}`, its account's token → `{ id, url, at, generation, calledAt, attempts, lastError }`
195
+ * - `DELETE /wakeup/instances/{id}`, its account's token → 204
196
+ * - `PUT /wakeup/wake { at, generation }`, an instance's own token, which names the instance → 204,
197
+ * applied only when `generation` is newer than the one it has, and waking the loop
198
+ *
199
+ * 401 without a token or with an unknown one, 403 for another account's instance, 400 for a bad
200
+ * body, 404 for an unknown id or path.
201
+ */
202
+ export async function startWakeupService({ databaseUrl, now = () => new Date(), fetch = globalThis.fetch, log = { info, warn }, }) {
203
+ await withTables(databaseUrl, async () => { });
204
+ const pool = openPool(databaseUrl, "wakeup service");
205
+ const loop = startLoop({ pool, now, fetch, log });
206
+ /** The live account the request's token is, or a 401. */
207
+ async function account(request) {
208
+ const token = bearer(request);
209
+ if (!token?.startsWith("swa_"))
210
+ return problem(401, "an account's token is needed");
211
+ const { rows } = await pool.query(`SELECT id FROM ${WAKEUP_ACCOUNTS_TABLE} WHERE token_hash = $1 AND revoked_at IS NULL`, [hashOf(token)]);
212
+ return rows[0]?.id ?? problem(401, "the token is no account's");
213
+ }
214
+ /** The account's instance `id`, or a 404, or a 403 when it is another account's. */
215
+ async function owned(accountId, id) {
216
+ if (!UUID.test(id))
217
+ return problem(404, "no such instance");
218
+ const { rows } = await pool.query(`SELECT * FROM ${WAKEUP_INSTANCES_TABLE} WHERE id = $1`, [id]);
219
+ const row = rows[0];
220
+ if (!row)
221
+ return problem(404, "no such instance");
222
+ return row.account_id === accountId ? row : problem(403, "the instance is another account's");
223
+ }
224
+ async function register(request) {
225
+ const accountId = await account(request);
226
+ if (accountId instanceof Response)
227
+ return accountId;
228
+ const given = await jsonBody(request);
229
+ if (given instanceof Response)
230
+ return given;
231
+ const checked = instanceUrl(given?.url);
232
+ if ("error" in checked)
233
+ return problem(400, checked.error);
234
+ const token = given?.token;
235
+ if (typeof token !== "string" || !INSTANCE_TOKEN.test(token))
236
+ return problem(400, "token must be an instance's token: swi_, then 43 or more base64url characters");
237
+ // The token names the instance: a new one is a new row, one of this account's is moved to the
238
+ // URL given, and another account's is refused (no row returned).
239
+ const { rows } = await pool.query(`INSERT INTO ${WAKEUP_INSTANCES_TABLE} AS i (id, account_id, url, token_hash) VALUES ($1, $2, $3, $4) ` +
240
+ "ON CONFLICT (token_hash) DO UPDATE SET url = EXCLUDED.url, " +
241
+ "at = CASE WHEN i.url = EXCLUDED.url THEN i.at END, " +
242
+ "generation = CASE WHEN i.url = EXCLUDED.url THEN i.generation ELSE 0 END, " +
243
+ "called_at = CASE WHEN i.url = EXCLUDED.url THEN i.called_at END, " +
244
+ "attempts = CASE WHEN i.url = EXCLUDED.url THEN i.attempts ELSE 0 END, " +
245
+ "next_attempt_at = CASE WHEN i.url = EXCLUDED.url THEN i.next_attempt_at END, " +
246
+ "last_error = CASE WHEN i.url = EXCLUDED.url THEN i.last_error END " +
247
+ "WHERE i.account_id = EXCLUDED.account_id RETURNING i.id, i.id = $1 AS made", [randomUUID(), accountId, checked.url, hashOf(token)]);
248
+ const row = rows[0];
249
+ if (!row) {
250
+ return problem(409, "the token is another account's instance's");
251
+ }
252
+ return json(row.made ? 201 : 200, { id: row.id });
253
+ }
254
+ async function wake(request) {
255
+ const token = bearer(request);
256
+ if (!token?.startsWith("swi_"))
257
+ return problem(401, "an instance's token is needed");
258
+ const { rows } = await pool.query(`SELECT i.id FROM ${WAKEUP_INSTANCES_TABLE} i ${LIVE} WHERE i.token_hash = $1`, [hashOf(token)]);
259
+ const id = rows[0]?.id;
260
+ if (!id)
261
+ return problem(401, "the token is no instance's");
262
+ const given = await jsonBody(request);
263
+ if (given instanceof Response)
264
+ return given;
265
+ const at = given?.at;
266
+ const generation = given?.generation;
267
+ if (typeof at !== "string" || !ISO_DATE_TIME.test(at) || Number.isNaN(Date.parse(at)))
268
+ return problem(400, "at must be an ISO 8601 date-time");
269
+ // A new instance has generation 0, so a registration counts from 1.
270
+ if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 1)
271
+ return problem(400, "generation must be an integer, 1 or more");
272
+ // An older or equal generation changes nothing: callers retry, at least once each.
273
+ await pool.query(`UPDATE ${WAKEUP_INSTANCES_TABLE} SET at = $2, generation = $3, called_at = NULL, attempts = 0, ` +
274
+ "next_attempt_at = NULL, last_error = NULL WHERE id = $1 AND generation < $3", [id, new Date(at), generation]);
275
+ loop.poke();
276
+ return noContent();
277
+ }
278
+ async function handle(request) {
279
+ const path = new URL(request.url).pathname;
280
+ const method = request.method;
281
+ if (path === "/wakeup/instances") {
282
+ return method === "POST" ? register(request) : notAllowed("POST");
283
+ }
284
+ // Its token says which instance it is.
285
+ if (path === "/wakeup/wake")
286
+ return method === "PUT" ? wake(request) : notAllowed("PUT");
287
+ const match = /^\/wakeup\/instances\/([^/]+)$/.exec(path);
288
+ if (!match)
289
+ return problem(404, "no such route");
290
+ const id = match[1];
291
+ if (method !== "GET" && method !== "DELETE")
292
+ return notAllowed("GET, DELETE");
293
+ const accountId = await account(request);
294
+ if (accountId instanceof Response)
295
+ return accountId;
296
+ const row = await owned(accountId, id);
297
+ if (row instanceof Response)
298
+ return row;
299
+ if (method === "GET")
300
+ return json(200, shownInstance(row));
301
+ await pool.query(`DELETE FROM ${WAKEUP_INSTANCES_TABLE} WHERE id = $1`, [id]);
302
+ return noContent();
303
+ }
304
+ return {
305
+ routes: [{ path: "/wakeup", prefix: true, handle }],
306
+ mintAccount: (label) => insertAccount(pool, label),
307
+ async revokeAccount(id) {
308
+ if (!UUID.test(id))
309
+ return;
310
+ await pool.query(`UPDATE ${WAKEUP_ACCOUNTS_TABLE} SET revoked_at = $2 WHERE id = $1 AND revoked_at IS NULL`, [
311
+ id,
312
+ now(),
313
+ ]);
314
+ },
315
+ async stop() {
316
+ await loop.stop();
317
+ await pool.end();
318
+ },
319
+ };
320
+ }
321
+ /**
322
+ * The one loop: calls every row that is due, at most `CONCURRENCY` at once, then sleeps until the
323
+ * next is (at most `MAX_SLEEP_MS`) or until `poke`. A failed call is tried again after
324
+ * `min(2^attempts s, MAX_BACKOFF_S)`, for as long as it takes: no row is ever dropped.
325
+ */
326
+ function startLoop({ pool, now, fetch, log, }) {
327
+ const stopping = new AbortController();
328
+ let poked = false;
329
+ let wakeUp;
330
+ /** Calls `row`'s instance once, and records how it went, unless the registration changed meanwhile. */
331
+ async function call(row) {
332
+ const target = `${row.url}/wake?generation=${row.generation}`;
333
+ let error;
334
+ try {
335
+ const signal = AbortSignal.any([stopping.signal, AbortSignal.timeout(CALL_TIMEOUT_MS)]);
336
+ // A redirect is no answer: it could send the service anywhere, so it is tried again as a failure.
337
+ const res = await fetch(target, { signal, redirect: "manual" });
338
+ await res.body?.cancel().catch(() => undefined);
339
+ if (!res.ok)
340
+ error = `answered ${res.status}`;
341
+ }
342
+ catch (err) {
343
+ error = errorMessage(err);
344
+ }
345
+ // Stopped mid-call: nothing is known of it, so it is called again once its claim lapses.
346
+ if (stopping.signal.aborted)
347
+ return;
348
+ // Recorded only on the registration called: one replaced (a newer generation), or moved to
349
+ // another URL (its generation counted again from 0), is called again.
350
+ const same = "WHERE id = $1 AND generation = $2 AND url = $3";
351
+ if (error === undefined) {
352
+ await pool.query(`UPDATE ${WAKEUP_INSTANCES_TABLE} SET called_at = $4, attempts = 0, next_attempt_at = NULL, ` +
353
+ `last_error = NULL ${same}`, [row.id, row.generation, row.url, now()]);
354
+ log.info(`wakeup: called ${target}`);
355
+ }
356
+ else {
357
+ await pool.query(`UPDATE ${WAKEUP_INSTANCES_TABLE} SET attempts = attempts + 1, last_error = $4, ` +
358
+ `next_attempt_at = $5::timestamptz + least(power(2, attempts), ${MAX_BACKOFF_S}) * interval '1 second' ` +
359
+ same, [row.id, row.generation, row.url, error, now()]);
360
+ log.warn(`wakeup: calling ${target} failed (${error}); it is tried again`);
361
+ }
362
+ }
363
+ /** Calls every due row; returns when the next one is due, or undefined with none waiting. */
364
+ async function round() {
365
+ // Taken for `CLAIM_S`, rows another service holds skipped: each due row is called by one service.
366
+ const { rows } = await pool.query(`UPDATE ${WAKEUP_INSTANCES_TABLE} SET next_attempt_at = $1::timestamptz + interval '${CLAIM_S} seconds' ` +
367
+ `WHERE id IN (SELECT i.id FROM ${WAKEUP_INSTANCES_TABLE} i ${LIVE} WHERE ${WAITING} ` +
368
+ "AND i.at <= $1 AND (i.next_attempt_at IS NULL OR i.next_attempt_at <= $1) " +
369
+ `ORDER BY i.at LIMIT ${ROUND_ROWS} FOR UPDATE OF i SKIP LOCKED) RETURNING id, url, generation`, [now()]);
370
+ const queue = [...rows];
371
+ const worker = async () => {
372
+ for (let row = queue.shift(); row && !stopping.signal.aborted; row = queue.shift()) {
373
+ await call(row).catch((err) => log.warn(`wakeup: could not record a call: ${errorMessage(err)}`));
374
+ }
375
+ };
376
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
377
+ const next = await pool.query(`SELECT min(greatest(i.at, coalesce(i.next_attempt_at, i.at))) AS next FROM ${WAKEUP_INSTANCES_TABLE} i ` +
378
+ `${LIVE} WHERE ${WAITING}`);
379
+ return next.rows[0]?.next ?? undefined;
380
+ }
381
+ const sleep = (ms) => new Promise((resolve) => {
382
+ const timer = setTimeout(resolve, ms);
383
+ wakeUp = () => {
384
+ clearTimeout(timer);
385
+ resolve();
386
+ };
387
+ });
388
+ const running = (async () => {
389
+ while (!stopping.signal.aborted) {
390
+ poked = false;
391
+ let ms = MAX_SLEEP_MS;
392
+ try {
393
+ const next = await round();
394
+ if (next)
395
+ ms = Math.min(Math.max(next.getTime() - now().getTime(), 0), MAX_SLEEP_MS);
396
+ }
397
+ catch (err) {
398
+ if (stopping.signal.aborted)
399
+ break;
400
+ log.warn(`wakeup: the loop could not read its instances: ${errorMessage(err)}`);
401
+ ms = 5_000;
402
+ }
403
+ // A poke during the round may have come after its last read: look again at once.
404
+ if (!poked && !stopping.signal.aborted)
405
+ await sleep(ms);
406
+ }
407
+ })();
408
+ return {
409
+ /** Wakes the loop now, or right after the round in progress. */
410
+ poke() {
411
+ poked = true;
412
+ wakeUp?.();
413
+ },
414
+ async stop() {
415
+ stopping.abort();
416
+ wakeUp?.();
417
+ await running;
418
+ },
419
+ };
420
+ }
421
+ //# sourceMappingURL=wakeup-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wakeup-service.js","sourceRoot":"","sources":["../src/wakeup-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import type { z } from "zod";
2
+ import type { Op } from "./op.ts";
3
+ import type { References, Resource } from "./resource.ts";
4
+ import type { DeclaredResource } from "./resources.ts";
5
+ /** What a wakeup's state says of it. */
6
+ export interface WakeupOutputs {
7
+ healthy: boolean;
8
+ }
9
+ /** What the runtime calls a wakeup's `register` operation with. */
10
+ export interface WakeupRegistration {
11
+ /** When to wake the instance, as an ISO 8601 date-time. */
12
+ at: string;
13
+ /** Counts up with each registration: one with a newer generation replaces the older. */
14
+ generation: number;
15
+ }
16
+ /**
17
+ * The `generation` of a `GET /wake?generation=N` request: a non-negative integer, in digits
18
+ * alone; undefined for none or any other.
19
+ */
20
+ export declare function wakeGeneration(url: string | URL): number | undefined;
21
+ /**
22
+ * A resource type's `wakeup`: the field it names the host in, the operation the runtime
23
+ * registers each deadline through, and how its state reads.
24
+ */
25
+ export interface WakeupSpec {
26
+ inputs: {
27
+ /**
28
+ * The dotted path of the declared field that names the host's declaration: a
29
+ * `fields.references` entry, resolved `by` a field of the host's state holding its URL.
30
+ */
31
+ host: string;
32
+ };
33
+ /**
34
+ * The id of the type's own register operation, `<vendor>.<type>.<op>`: an operation in the
35
+ * type's own group of its connector, beside `read` and `import` (the way Supabase adds
36
+ * `project.health` beside `project`). The runtime calls it with `WakeupRegistration`,
37
+ * at least once per registration; a newer `generation` replaces the older.
38
+ */
39
+ register: string;
40
+ outputs: (state: Record<string, unknown>) => WakeupOutputs;
41
+ }
42
+ /** The operations `defineResource` gives a type itself: never its register, or a database's `storeUrl`. */
43
+ export declare const RESOURCE_OPS: Set<string>;
44
+ /**
45
+ * Throws what is wrong with a resource type's `wakeup`: a type apply cannot change (not
46
+ * `writable`), a host (stopped when its wake is due), a `host` that names no field or no
47
+ * reference resolved `by` a field, a `register` that is not an operation of the type's own group
48
+ * or is one of its resource operations, `outputs` that is not a function. Whether the connector
49
+ * has that operation, and the reference names a host, is `checkWakeupOps`'s, which
50
+ * `resolveConfig` runs.
51
+ */
52
+ export declare function checkWakeup(what: string, wakeup: WakeupSpec, type: {
53
+ vendor: string;
54
+ type: string;
55
+ schema: z.ZodObject;
56
+ fields: {
57
+ references?: References;
58
+ };
59
+ writable?: boolean;
60
+ host?: unknown;
61
+ }): void;
62
+ /** The host types a wakeup type may name: its `inputs.host` reference's. */
63
+ export declare const wakeupHostTypes: (type: Pick<Resource, "fields" | "wakeup">) => string[];
64
+ /**
65
+ * Throws when a wakeup type's host reference names a type of the config that is no host, or its
66
+ * `register` is not among the config's operations, or does not take
67
+ * `WakeupRegistration`. Here rather than in `defineConnector`, which sees one connector before
68
+ * its package merges in the operations it adds to a resource's group (Supabase's pattern).
69
+ */
70
+ export declare function checkWakeupOps(types: ReadonlyMap<string, Resource>, ops: ReadonlyMap<string, Op>): void;
71
+ /** The one wakeup the data files declare: its declaration, its type, and its binding when it is bound. */
72
+ export interface DeclaredWakeup {
73
+ /** `<file>#<export>`, as `DeclaredResource.id`. */
74
+ id: string;
75
+ type: Resource;
76
+ declared: DeclaredResource;
77
+ /** The vendor's id of the object the lockfile binds it to; absent when unbound. */
78
+ binding?: string;
79
+ /** The declared host its `inputs.host` names: its declaration's id and its type, a host's (`checkWakeupOps`). */
80
+ host?: {
81
+ id: string;
82
+ type: Resource;
83
+ };
84
+ }