@isomorph.ai/app-sdk 1.4.0 → 1.4.1

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/dist/runtime.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare class Runs {
39
39
  private readonly client;
40
40
  constructor(client: IsomorphClient);
41
41
  private table;
42
+ /** The cheapest read the runs table answers; rejects while the gateway's data path is down. */
43
+ ready(): Promise<void>;
42
44
  enqueue(job: string, input: unknown, owner: string): Promise<{
43
45
  id: string;
44
46
  }>;
@@ -86,10 +88,11 @@ export declare class Runtime {
86
88
  * `--run-job <name>`: one scheduled run of one job, as a CronJob pod runs it.
87
89
  * The run is the scheduler's own (`runScheduled`, the current minute as its
88
90
  * tick), so it is loaded, run and recorded exactly as an in-process tick
89
- * would have been. False when the app has no such job or the handler threw;
90
- * the log names which.
91
+ * would have been. False when the app has no such job, the gateway's data
92
+ * path never answered, or the handler threw; the log names which.
91
93
  */
92
94
  runJobOnce(name: string, now?: Date): Promise<boolean>;
95
+ private awaitDataPath;
93
96
  stop(): Promise<void>;
94
97
  private every;
95
98
  private refreshToken;
package/dist/runtime.js CHANGED
@@ -54,6 +54,10 @@ const LEASE_MS = 5 * 60_000;
54
54
  const QUEUE_POLL_MS = 2_000;
55
55
  const TOKEN_REFRESH_MS = 60_000;
56
56
  const DEFAULT_MAX_ATTEMPTS = 3;
57
+ // A CronJob pod may start while the app's database is still waking: --run-job
58
+ // waits this long for the gateway's data path, probing every retry interval.
59
+ const DATA_PATH_WAIT_MS = 90_000;
60
+ const DATA_PATH_RETRY_MS = 3_000;
57
61
  /** Every `actions/<name>.ts` and `jobs/<name>.ts` the app carries, by logical name. */
58
62
  export async function discover(root) {
59
63
  const list = async (dir) => {
@@ -150,6 +154,10 @@ export class Runs {
150
154
  this.client = client;
151
155
  }
152
156
  table() { return this.client.data.from(RUNS_TABLE); }
157
+ /** The cheapest read the runs table answers; rejects while the gateway's data path is down. */
158
+ async ready() {
159
+ await this.table().select('id').limit(1);
160
+ }
153
161
  async enqueue(job, input, owner) {
154
162
  const { data } = await this.table().insert({ job, input: input ?? null, status: 'queued', attempt: 0, max_attempts: DEFAULT_MAX_ATTEMPTS, progress: 0, owner_subject: owner, cancel_requested: false }).select('id').single();
155
163
  return { id: data.id };
@@ -232,8 +240,8 @@ export class Runtime {
232
240
  * `--run-job <name>`: one scheduled run of one job, as a CronJob pod runs it.
233
241
  * The run is the scheduler's own (`runScheduled`, the current minute as its
234
242
  * tick), so it is loaded, run and recorded exactly as an in-process tick
235
- * would have been. False when the app has no such job or the handler threw;
236
- * the log names which.
243
+ * would have been. False when the app has no such job, the gateway's data
244
+ * path never answered, or the handler threw; the log names which.
237
245
  */
238
246
  async runJobOnce(name, now = new Date()) {
239
247
  this.inventory = await discover(this.options.root);
@@ -242,8 +250,32 @@ export class Runtime {
242
250
  this.log(`this app has no ${JOBS_DIR}/${name}.ts`);
243
251
  return false;
244
252
  }
253
+ if (this.options.gatewayUrl && !(await this.awaitDataPath()))
254
+ return false;
245
255
  return this.runScheduled(name, new Date(Math.floor(now.getTime() / 60_000) * 60_000));
246
256
  }
257
+ // The pod is scheduled by the cluster clock, not by the app's readiness: its
258
+ // database may be paused or waking (plan §5.3) and the gateway answers 5xx
259
+ // until it is up. The handler runs once the cheapest read answers; when it
260
+ // never does within the wait, the run fails with one line naming the last error.
261
+ async awaitDataPath() {
262
+ const deadline = Date.now() + DATA_PATH_WAIT_MS;
263
+ for (;;) {
264
+ try {
265
+ await this.runs.ready();
266
+ return true;
267
+ }
268
+ catch (error) {
269
+ const reason = error instanceof Error ? error.message : String(error);
270
+ if (Date.now() + DATA_PATH_RETRY_MS > deadline) {
271
+ this.log(`the data path did not answer within ${DATA_PATH_WAIT_MS / 1000} s: ${reason}`);
272
+ return false;
273
+ }
274
+ this.log(`waiting for the data path: ${reason}`);
275
+ await new Promise((wake) => setTimeout(wake, DATA_PATH_RETRY_MS));
276
+ }
277
+ }
278
+ }
247
279
  async stop() {
248
280
  this.stopped = true;
249
281
  for (const timer of this.timers)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isomorph.ai/app-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",