@isomorph.ai/app-sdk 1.10.0 → 1.12.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/dist/index.js +17 -10
- package/dist/integration-operations.d.ts +87 -0
- package/dist/integration-operations.js +117 -0
- package/dist/jobs-cli.d.ts +1 -0
- package/dist/jobs-cli.js +11 -0
- package/dist/jobs.d.ts +17 -0
- package/dist/jobs.js +98 -0
- package/dist/runtime.d.ts +3 -12
- package/dist/runtime.js +5 -29
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IntegrationOperations } from './integration-operations.js';
|
|
1
2
|
import { validateIntegrationExecuteInput } from './integration-execute-input-rules.js';
|
|
2
3
|
import { userGroups } from './user-groups.js';
|
|
3
4
|
import { userRole } from './user-role.js';
|
|
@@ -29,13 +30,6 @@ function warehouseReadProblem({ columns, aggregates }) {
|
|
|
29
30
|
return { field: 'columns', message: 'columns may use "*" only as the sole entry' };
|
|
30
31
|
return undefined;
|
|
31
32
|
}
|
|
32
|
-
// The operations whose request carries a `mode`, and the only ones it is sent
|
|
33
|
-
// for (every other operation's mode is fixed, and the platform refuses the
|
|
34
|
-
// field there). The key type is derived from the union, so adding or removing
|
|
35
|
-
// `mode` on a request without changing this table does not compile.
|
|
36
|
-
const declaredModeOperations = {
|
|
37
|
-
'slack.channel.history': true, 'slack.message.post': true, 'slack.message.update': true, 'slack.message.delete': true,
|
|
38
|
-
};
|
|
39
33
|
/** The AI reason code carried by an error, when it is a governed-AI refusal. */
|
|
40
34
|
export function aiErrorCode(error) {
|
|
41
35
|
const code = error instanceof IsomorphError ? error.details?.code : undefined;
|
|
@@ -498,6 +492,19 @@ export function createClient(options = {}) {
|
|
|
498
492
|
},
|
|
499
493
|
};
|
|
500
494
|
}
|
|
495
|
+
// A kit page on its own API host pays for its first call three times over:
|
|
496
|
+
// a fresh connection (TCP, TLS, then a CORS preflight), the 401 → context
|
|
497
|
+
// refresh → resend detour once the context cookie has lapsed, and only then
|
|
498
|
+
// the call. Sending the identity GET now — the one the API host heals — opens
|
|
499
|
+
// the connection, renews a lapsed context and caches the user while the page
|
|
500
|
+
// is still rendering, so the first action rides an open connection with a
|
|
501
|
+
// live context; a refusal while it is in flight shares it (renew above). A
|
|
502
|
+
// page-relative base (isomorph dev, a git app on the shared edge) already has
|
|
503
|
+
// its connection and the edge heals its GETs, so nothing is sent there, and
|
|
504
|
+
// under Node the identity is a static token. Its outcome is the page's to
|
|
505
|
+
// read through identity.current() and onChange, never surfaced here.
|
|
506
|
+
if (transport.separateApiHost)
|
|
507
|
+
void refreshIdentity().catch(() => undefined);
|
|
501
508
|
const scheduleIdentityRefresh = () => {
|
|
502
509
|
if (refreshTimer !== undefined || authListeners.size === 0 || identityRefreshMs <= 0)
|
|
503
510
|
return;
|
|
@@ -726,11 +733,11 @@ export function createClient(options = {}) {
|
|
|
726
733
|
if (problem) {
|
|
727
734
|
return Promise.reject(new IsomorphError('VALIDATION_FAILED', problem.message, undefined, { code: 'INPUT_INVALID', field: problem.field }));
|
|
728
735
|
}
|
|
729
|
-
// Exactly the contract's fields go on the wire:
|
|
730
|
-
//
|
|
736
|
+
// Exactly the contract's fields go on the wire: metadata declares which
|
|
737
|
+
// operations accept `mode`; identity and stray fields are not sent.
|
|
731
738
|
const { operation, resource, input, idempotencyKey, mode } = request;
|
|
732
739
|
return transport.json('/_harbour/integrations/execute', {
|
|
733
|
-
method: 'POST', body: JSON.stringify({ connection, operation, resource, ...(input !== undefined ? { input } : {}), ...(idempotencyKey !== undefined ? { idempotencyKey } : {}), ...(operation
|
|
740
|
+
method: 'POST', body: JSON.stringify({ connection, operation, resource, ...(input !== undefined ? { input } : {}), ...(idempotencyKey !== undefined ? { idempotencyKey } : {}), ...(IntegrationOperations[operation]?.declaredMode && mode !== undefined ? { mode } : {}) }),
|
|
734
741
|
});
|
|
735
742
|
},
|
|
736
743
|
// In a browser the consent returns the person to this page (without its
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Declaration and execute metadata from the contract schemas. */
|
|
2
|
+
export declare const IntegrationOperations: {
|
|
3
|
+
readonly "slack.channel.history": {
|
|
4
|
+
readonly kind: "saas";
|
|
5
|
+
readonly identities: readonly ["user", "app"];
|
|
6
|
+
readonly readOnly: true;
|
|
7
|
+
readonly declaredMode: true;
|
|
8
|
+
readonly idempotencyKey: false;
|
|
9
|
+
};
|
|
10
|
+
readonly "slack.message.post": {
|
|
11
|
+
readonly kind: "saas";
|
|
12
|
+
readonly identities: readonly ["app", "user"];
|
|
13
|
+
readonly readOnly: false;
|
|
14
|
+
readonly declaredMode: true;
|
|
15
|
+
readonly idempotencyKey: true;
|
|
16
|
+
};
|
|
17
|
+
readonly "slack.message.update": {
|
|
18
|
+
readonly kind: "saas";
|
|
19
|
+
readonly identities: readonly ["app", "user"];
|
|
20
|
+
readonly readOnly: false;
|
|
21
|
+
readonly declaredMode: true;
|
|
22
|
+
readonly idempotencyKey: false;
|
|
23
|
+
};
|
|
24
|
+
readonly "slack.message.delete": {
|
|
25
|
+
readonly kind: "saas";
|
|
26
|
+
readonly identities: readonly ["app", "user"];
|
|
27
|
+
readonly readOnly: false;
|
|
28
|
+
readonly declaredMode: true;
|
|
29
|
+
readonly idempotencyKey: false;
|
|
30
|
+
};
|
|
31
|
+
readonly "slack.dm.send": {
|
|
32
|
+
readonly kind: "saas";
|
|
33
|
+
readonly identities: readonly ["app"];
|
|
34
|
+
readonly readOnly: false;
|
|
35
|
+
readonly declaredMode: false;
|
|
36
|
+
readonly idempotencyKey: true;
|
|
37
|
+
};
|
|
38
|
+
readonly "slack.user.lookup": {
|
|
39
|
+
readonly kind: "saas";
|
|
40
|
+
readonly identities: readonly ["app"];
|
|
41
|
+
readonly readOnly: true;
|
|
42
|
+
readonly declaredMode: false;
|
|
43
|
+
readonly idempotencyKey: false;
|
|
44
|
+
};
|
|
45
|
+
readonly "gmail.thread.list": {
|
|
46
|
+
readonly kind: "saas";
|
|
47
|
+
readonly identities: readonly ["user"];
|
|
48
|
+
readonly readOnly: true;
|
|
49
|
+
readonly declaredMode: false;
|
|
50
|
+
readonly idempotencyKey: false;
|
|
51
|
+
};
|
|
52
|
+
readonly "gmail.thread.read": {
|
|
53
|
+
readonly kind: "saas";
|
|
54
|
+
readonly identities: readonly ["user"];
|
|
55
|
+
readonly readOnly: true;
|
|
56
|
+
readonly declaredMode: false;
|
|
57
|
+
readonly idempotencyKey: false;
|
|
58
|
+
};
|
|
59
|
+
readonly "gmail.message.read": {
|
|
60
|
+
readonly kind: "saas";
|
|
61
|
+
readonly identities: readonly ["user"];
|
|
62
|
+
readonly readOnly: true;
|
|
63
|
+
readonly declaredMode: false;
|
|
64
|
+
readonly idempotencyKey: false;
|
|
65
|
+
};
|
|
66
|
+
readonly "gmail.message.send": {
|
|
67
|
+
readonly kind: "saas";
|
|
68
|
+
readonly identities: readonly ["user"];
|
|
69
|
+
readonly readOnly: false;
|
|
70
|
+
readonly declaredMode: false;
|
|
71
|
+
readonly idempotencyKey: true;
|
|
72
|
+
};
|
|
73
|
+
readonly "gmail.attachment.read": {
|
|
74
|
+
readonly kind: "saas";
|
|
75
|
+
readonly identities: readonly ["user"];
|
|
76
|
+
readonly readOnly: true;
|
|
77
|
+
readonly declaredMode: false;
|
|
78
|
+
readonly idempotencyKey: false;
|
|
79
|
+
};
|
|
80
|
+
readonly "warehouse.view.read": {
|
|
81
|
+
readonly kind: "database";
|
|
82
|
+
readonly identities: readonly ["app"];
|
|
83
|
+
readonly readOnly: true;
|
|
84
|
+
readonly declaredMode: false;
|
|
85
|
+
readonly idempotencyKey: false;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Code generated by tools/gen/gen.mjs from schema/*.json. DO NOT EDIT.
|
|
3
|
+
/** Declaration and execute metadata from the contract schemas. */
|
|
4
|
+
export const IntegrationOperations = {
|
|
5
|
+
"slack.channel.history": {
|
|
6
|
+
"kind": "saas",
|
|
7
|
+
"identities": [
|
|
8
|
+
"user",
|
|
9
|
+
"app"
|
|
10
|
+
],
|
|
11
|
+
"readOnly": true,
|
|
12
|
+
"declaredMode": true,
|
|
13
|
+
"idempotencyKey": false
|
|
14
|
+
},
|
|
15
|
+
"slack.message.post": {
|
|
16
|
+
"kind": "saas",
|
|
17
|
+
"identities": [
|
|
18
|
+
"app",
|
|
19
|
+
"user"
|
|
20
|
+
],
|
|
21
|
+
"readOnly": false,
|
|
22
|
+
"declaredMode": true,
|
|
23
|
+
"idempotencyKey": true
|
|
24
|
+
},
|
|
25
|
+
"slack.message.update": {
|
|
26
|
+
"kind": "saas",
|
|
27
|
+
"identities": [
|
|
28
|
+
"app",
|
|
29
|
+
"user"
|
|
30
|
+
],
|
|
31
|
+
"readOnly": false,
|
|
32
|
+
"declaredMode": true,
|
|
33
|
+
"idempotencyKey": false
|
|
34
|
+
},
|
|
35
|
+
"slack.message.delete": {
|
|
36
|
+
"kind": "saas",
|
|
37
|
+
"identities": [
|
|
38
|
+
"app",
|
|
39
|
+
"user"
|
|
40
|
+
],
|
|
41
|
+
"readOnly": false,
|
|
42
|
+
"declaredMode": true,
|
|
43
|
+
"idempotencyKey": false
|
|
44
|
+
},
|
|
45
|
+
"slack.dm.send": {
|
|
46
|
+
"kind": "saas",
|
|
47
|
+
"identities": [
|
|
48
|
+
"app"
|
|
49
|
+
],
|
|
50
|
+
"readOnly": false,
|
|
51
|
+
"declaredMode": false,
|
|
52
|
+
"idempotencyKey": true
|
|
53
|
+
},
|
|
54
|
+
"slack.user.lookup": {
|
|
55
|
+
"kind": "saas",
|
|
56
|
+
"identities": [
|
|
57
|
+
"app"
|
|
58
|
+
],
|
|
59
|
+
"readOnly": true,
|
|
60
|
+
"declaredMode": false,
|
|
61
|
+
"idempotencyKey": false
|
|
62
|
+
},
|
|
63
|
+
"gmail.thread.list": {
|
|
64
|
+
"kind": "saas",
|
|
65
|
+
"identities": [
|
|
66
|
+
"user"
|
|
67
|
+
],
|
|
68
|
+
"readOnly": true,
|
|
69
|
+
"declaredMode": false,
|
|
70
|
+
"idempotencyKey": false
|
|
71
|
+
},
|
|
72
|
+
"gmail.thread.read": {
|
|
73
|
+
"kind": "saas",
|
|
74
|
+
"identities": [
|
|
75
|
+
"user"
|
|
76
|
+
],
|
|
77
|
+
"readOnly": true,
|
|
78
|
+
"declaredMode": false,
|
|
79
|
+
"idempotencyKey": false
|
|
80
|
+
},
|
|
81
|
+
"gmail.message.read": {
|
|
82
|
+
"kind": "saas",
|
|
83
|
+
"identities": [
|
|
84
|
+
"user"
|
|
85
|
+
],
|
|
86
|
+
"readOnly": true,
|
|
87
|
+
"declaredMode": false,
|
|
88
|
+
"idempotencyKey": false
|
|
89
|
+
},
|
|
90
|
+
"gmail.message.send": {
|
|
91
|
+
"kind": "saas",
|
|
92
|
+
"identities": [
|
|
93
|
+
"user"
|
|
94
|
+
],
|
|
95
|
+
"readOnly": false,
|
|
96
|
+
"declaredMode": false,
|
|
97
|
+
"idempotencyKey": true
|
|
98
|
+
},
|
|
99
|
+
"gmail.attachment.read": {
|
|
100
|
+
"kind": "saas",
|
|
101
|
+
"identities": [
|
|
102
|
+
"user"
|
|
103
|
+
],
|
|
104
|
+
"readOnly": true,
|
|
105
|
+
"declaredMode": false,
|
|
106
|
+
"idempotencyKey": false
|
|
107
|
+
},
|
|
108
|
+
"warehouse.view.read": {
|
|
109
|
+
"kind": "database",
|
|
110
|
+
"identities": [
|
|
111
|
+
"app"
|
|
112
|
+
],
|
|
113
|
+
"readOnly": true,
|
|
114
|
+
"declaredMode": false,
|
|
115
|
+
"idempotencyKey": false
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/jobs-cli.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Read-only native gate adapter. No app modules or runtime server are started.
|
|
2
|
+
import { discoverJobs } from './jobs.js';
|
|
3
|
+
try {
|
|
4
|
+
if (process.argv[2] !== 'discover' || !process.argv[3])
|
|
5
|
+
throw new Error('usage: jobs-cli.js discover <app-root>');
|
|
6
|
+
process.stdout.write(JSON.stringify(await discoverJobs(process.argv[3])) + '\n');
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
}
|
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const JOBS_DIR = "jobs";
|
|
2
|
+
export interface JobDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
schedule?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface JobContext {
|
|
7
|
+
scheduledAt: string;
|
|
8
|
+
runId?: string;
|
|
9
|
+
input?: unknown;
|
|
10
|
+
progress(percent: number, note?: string): Promise<void>;
|
|
11
|
+
cancelled(): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
export declare function discoverJobs(root: string): Promise<JobDefinition[]>;
|
|
14
|
+
type Handler = (...args: unknown[]) => unknown;
|
|
15
|
+
export declare function loadHandler(root: string, dir: string, name: string): Promise<Handler>;
|
|
16
|
+
export declare function invokeJob(root: string, name: string, context: JobContext): Promise<unknown>;
|
|
17
|
+
export {};
|
package/dist/jobs.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Node-only: shared by runtime, local CLI and the native gate. Discovery never imports app code.
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { stripTypeScriptTypes } from 'node:module';
|
|
6
|
+
import { parse } from 'acorn';
|
|
7
|
+
export const JOBS_DIR = 'jobs';
|
|
8
|
+
const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9
|
+
function bindsSchedule(binding) {
|
|
10
|
+
switch (binding.type) {
|
|
11
|
+
case 'Identifier': return binding.name === 'schedule';
|
|
12
|
+
case 'ObjectPattern': return binding.properties.some(property => bindsSchedule(property.type === 'RestElement' ? property.argument : property.value));
|
|
13
|
+
case 'ArrayPattern': return binding.elements.some(element => element !== null && bindsSchedule(element));
|
|
14
|
+
case 'AssignmentPattern': return bindsSchedule(binding.left);
|
|
15
|
+
case 'RestElement': return bindsSchedule(binding.argument);
|
|
16
|
+
default: return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function discoverJobs(root) {
|
|
20
|
+
const directory = join(root, JOBS_DIR);
|
|
21
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
22
|
+
if (error.code === 'ENOENT')
|
|
23
|
+
return [];
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
const jobs = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isFile() || !entry.name.endsWith('.ts'))
|
|
29
|
+
continue;
|
|
30
|
+
const name = entry.name.slice(0, -3);
|
|
31
|
+
const fail = (message) => { throw new Error(`jobs/${entry.name}: ${message}`); };
|
|
32
|
+
if (!LOGICAL_NAME.test(name))
|
|
33
|
+
fail('the file name must be a lowercase logical name.');
|
|
34
|
+
const source = await readFile(join(directory, entry.name), 'utf8');
|
|
35
|
+
let statements;
|
|
36
|
+
try {
|
|
37
|
+
statements = parse(stripTypeScriptTypes(source), { ecmaVersion: 'latest', sourceType: 'module' }).body;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
fail(`cannot parse the job module: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
let schedule;
|
|
43
|
+
for (const statement of statements) {
|
|
44
|
+
if (statement.type === 'ExportAllDeclaration' && statement.exported && (statement.exported.type === 'Identifier' ? statement.exported.name : statement.exported.value) === 'schedule')
|
|
45
|
+
fail('export one literal schedule directly with export const schedule.');
|
|
46
|
+
if (statement.type !== 'ExportNamedDeclaration')
|
|
47
|
+
continue;
|
|
48
|
+
if (statement.specifiers.some(specifier => specifier.exported.type === 'Identifier' ? specifier.exported.name === 'schedule' : specifier.exported.value === 'schedule'))
|
|
49
|
+
fail('export one literal schedule directly with export const schedule.');
|
|
50
|
+
if (statement.declaration?.type !== 'VariableDeclaration') {
|
|
51
|
+
if (statement.declaration?.id?.name === 'schedule')
|
|
52
|
+
fail('export one literal schedule directly with export const schedule.');
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const declaration of statement.declaration.declarations) {
|
|
56
|
+
if (!bindsSchedule(declaration.id))
|
|
57
|
+
continue;
|
|
58
|
+
if (declaration.id.type !== 'Identifier')
|
|
59
|
+
fail('export one literal schedule directly with export const schedule.');
|
|
60
|
+
const value = declaration.init;
|
|
61
|
+
if (statement.declaration.kind !== 'const')
|
|
62
|
+
fail('export one literal schedule.');
|
|
63
|
+
if (value?.type === 'Literal' && typeof value.value === 'string')
|
|
64
|
+
schedule = value.value.trim();
|
|
65
|
+
else if (value?.type === 'TemplateLiteral' && value.expressions.length === 0)
|
|
66
|
+
schedule = value.quasis[0].value.cooked?.trim();
|
|
67
|
+
else
|
|
68
|
+
fail('export one literal schedule, for example export const schedule = "0 9 * * 1-5".');
|
|
69
|
+
if (!schedule)
|
|
70
|
+
fail('export one non-empty literal schedule.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!statements.some(statement => statement.type === 'ExportDefaultDeclaration'))
|
|
74
|
+
fail('export one default job handler.');
|
|
75
|
+
jobs.push({ name, ...(schedule ? { schedule } : {}) });
|
|
76
|
+
}
|
|
77
|
+
return jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
78
|
+
}
|
|
79
|
+
const modules = new Map();
|
|
80
|
+
export function loadHandler(root, dir, name) {
|
|
81
|
+
const key = pathToFileURL(join(root, dir, `${name}.ts`)).href;
|
|
82
|
+
let loading = modules.get(key);
|
|
83
|
+
if (!loading) {
|
|
84
|
+
loading = import(key).then((module) => {
|
|
85
|
+
if (typeof module.default !== 'function')
|
|
86
|
+
throw new Error(`${dir}/${name}.ts has no default handler`);
|
|
87
|
+
return module.default;
|
|
88
|
+
});
|
|
89
|
+
loading.catch(() => modules.delete(key));
|
|
90
|
+
modules.set(key, loading);
|
|
91
|
+
}
|
|
92
|
+
return loading;
|
|
93
|
+
}
|
|
94
|
+
export async function invokeJob(root, name, context) {
|
|
95
|
+
if (!LOGICAL_NAME.test(name))
|
|
96
|
+
throw new Error('A valid job name is required.');
|
|
97
|
+
return (await loadHandler(root, JOBS_DIR, name))(context);
|
|
98
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import { type IsomorphClient, type IsomorphUser, type JobRun } from './index.js';
|
|
3
|
+
import { type JobDefinition } from './jobs.js';
|
|
4
|
+
export { JOBS_DIR, type JobContext } from './jobs.js';
|
|
3
5
|
export declare const ACTIONS_DIR = "actions";
|
|
4
|
-
export declare const JOBS_DIR = "jobs";
|
|
5
6
|
export declare const RUNS_TABLE = "isomorph_job_runs";
|
|
6
7
|
export declare const RESERVED_PREFIX = "isomorph-";
|
|
7
8
|
export declare const PING_ACTION = "isomorph-ping";
|
|
@@ -21,19 +22,9 @@ export interface ActionContext {
|
|
|
21
22
|
*/
|
|
22
23
|
progress(step: string | Record<string, unknown>): void;
|
|
23
24
|
}
|
|
24
|
-
export interface JobContext {
|
|
25
|
-
scheduledAt: string;
|
|
26
|
-
runId?: string;
|
|
27
|
-
input?: unknown;
|
|
28
|
-
progress(percent: number, note?: string): Promise<void>;
|
|
29
|
-
cancelled(): Promise<boolean>;
|
|
30
|
-
}
|
|
31
25
|
export interface RuntimeInventory {
|
|
32
26
|
actions: string[];
|
|
33
|
-
jobs:
|
|
34
|
-
name: string;
|
|
35
|
-
schedule?: string;
|
|
36
|
-
}[];
|
|
27
|
+
jobs: JobDefinition[];
|
|
37
28
|
}
|
|
38
29
|
/** Every `actions/<name>.ts` and `jobs/<name>.ts` the app carries, by logical name. */
|
|
39
30
|
export declare function discover(root: string): Promise<RuntimeInventory>;
|
package/dist/runtime.js
CHANGED
|
@@ -58,11 +58,12 @@ import { pathToFileURL } from 'node:url';
|
|
|
58
58
|
import { createClient, IsomorphError } from './index.js';
|
|
59
59
|
import { userGroupsFromHeader } from './user-groups.js';
|
|
60
60
|
import { userRole } from './user-role.js';
|
|
61
|
+
import { discoverJobs, invokeJob, loadHandler, JOBS_DIR } from './jobs.js';
|
|
62
|
+
export { JOBS_DIR } from './jobs.js';
|
|
61
63
|
const actionIdentity = new AsyncLocalStorage();
|
|
62
64
|
const identityKey = Symbol.for('isomorph.actionIdentity');
|
|
63
65
|
globalThis[identityKey] = () => actionIdentity.getStore();
|
|
64
66
|
export const ACTIONS_DIR = 'actions';
|
|
65
|
-
export const JOBS_DIR = 'jobs';
|
|
66
67
|
export const RUNS_TABLE = 'isomorph_job_runs';
|
|
67
68
|
export const RESERVED_PREFIX = 'isomorph-';
|
|
68
69
|
export const PING_ACTION = 'isomorph-ping';
|
|
@@ -70,9 +71,6 @@ export const ENQUEUE_ACTION = 'isomorph-jobs-enqueue';
|
|
|
70
71
|
export const GET_RUN_ACTION = 'isomorph-jobs-get';
|
|
71
72
|
export const CANCEL_RUN_ACTION = 'isomorph-jobs-cancel';
|
|
72
73
|
const LOGICAL_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
73
|
-
// \x60 is the backtick: the brand guard counts template literals by
|
|
74
|
-
// backticks, and a bare one inside this regex unbalanced its scan.
|
|
75
|
-
const SCHEDULE_DECLARATION = /^export\s+const\s+schedule\s*=\s*["'\x60]([^"'\x60\n]+)["'\x60]/m;
|
|
76
74
|
const MAX_BODY_BYTES = 1 << 20;
|
|
77
75
|
const LEASE_MS = 5 * 60_000;
|
|
78
76
|
const QUEUE_POLL_MS = 2_000;
|
|
@@ -95,12 +93,7 @@ export async function discover(root) {
|
|
|
95
93
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith('.ts')).map((entry) => entry.name.slice(0, -3)).filter((name) => LOGICAL_NAME.test(name)).sort();
|
|
96
94
|
};
|
|
97
95
|
const actions = (await list(ACTIONS_DIR)).filter((name) => !name.startsWith(RESERVED_PREFIX));
|
|
98
|
-
const jobs =
|
|
99
|
-
for (const name of await list(JOBS_DIR)) {
|
|
100
|
-
const body = await readFile(join(root, JOBS_DIR, `${name}.ts`), 'utf8');
|
|
101
|
-
const match = SCHEDULE_DECLARATION.exec(body);
|
|
102
|
-
jobs.push(match ? { name, schedule: match[1].trim() } : { name });
|
|
103
|
-
}
|
|
96
|
+
const jobs = await discoverJobs(root);
|
|
104
97
|
return { actions, jobs };
|
|
105
98
|
}
|
|
106
99
|
// ---- Cron ------------------------------------------------------------------
|
|
@@ -138,21 +131,6 @@ export function cronMatches(expression, at) {
|
|
|
138
131
|
&& fieldMatches(month, at.getUTCMonth() + 1, 1, 12)
|
|
139
132
|
&& (fieldMatches(weekday, at.getUTCDay(), 0, 7) || (sunday && fieldMatches(weekday, 7, 0, 7)));
|
|
140
133
|
}
|
|
141
|
-
const modules = new Map();
|
|
142
|
-
function loadHandler(root, dir, name) {
|
|
143
|
-
const key = `${dir}/${name}`;
|
|
144
|
-
let loading = modules.get(key);
|
|
145
|
-
if (!loading) {
|
|
146
|
-
loading = import(pathToFileURL(join(root, dir, `${name}.ts`)).href).then((module) => {
|
|
147
|
-
if (typeof module.default !== 'function')
|
|
148
|
-
throw new Error(`${key}.ts has no default handler`);
|
|
149
|
-
return module.default;
|
|
150
|
-
});
|
|
151
|
-
loading.catch(() => modules.delete(key));
|
|
152
|
-
modules.set(key, loading);
|
|
153
|
-
}
|
|
154
|
-
return loading;
|
|
155
|
-
}
|
|
156
134
|
// ---- HTTP helpers ------------------------------------------------------------
|
|
157
135
|
// Once an action has streamed progress the status line is gone, so its
|
|
158
136
|
// answer — result or refusal — is the stream's last line, the refusal
|
|
@@ -447,8 +425,7 @@ export class Runtime {
|
|
|
447
425
|
this.running.add(key);
|
|
448
426
|
try {
|
|
449
427
|
await this.prepareAI();
|
|
450
|
-
|
|
451
|
-
await handler({ scheduledAt: at.toISOString(), progress: async () => undefined, cancelled: async () => false });
|
|
428
|
+
await invokeJob(this.options.root, name, { scheduledAt: at.toISOString(), progress: async () => undefined, cancelled: async () => false });
|
|
452
429
|
this.log(`jobs/${name}.ts ran for ${at.toISOString()}`);
|
|
453
430
|
return true;
|
|
454
431
|
}
|
|
@@ -482,8 +459,7 @@ export class Runtime {
|
|
|
482
459
|
}
|
|
483
460
|
try {
|
|
484
461
|
await this.prepareAI();
|
|
485
|
-
const
|
|
486
|
-
const result = await handler({
|
|
462
|
+
const result = await invokeJob(this.options.root, run.job, {
|
|
487
463
|
scheduledAt: now.toISOString(), runId: run.id, input: run.input,
|
|
488
464
|
progress: (percent, note) => this.runs.progress(run.id, percent, note),
|
|
489
465
|
cancelled: () => this.runs.cancelRequested(run.id),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isomorph.ai/app-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,5 +36,8 @@
|
|
|
36
36
|
"playwright-core": {
|
|
37
37
|
"optional": true
|
|
38
38
|
}
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"acorn": "8.18.0"
|
|
39
42
|
}
|
|
40
43
|
}
|