@metacall/protocol 0.1.26 → 0.1.28

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.d.ts CHANGED
@@ -3,7 +3,8 @@ export * from './language';
3
3
  export * from './login';
4
4
  export * from './package';
5
5
  export * from './plan';
6
+ export * from './protocol';
7
+ export * from './signup';
6
8
  export * from './token';
7
9
  import metacallAPI from './protocol';
8
- export * from './protocol';
9
10
  export default metacallAPI;
package/dist/index.js CHANGED
@@ -18,7 +18,8 @@ __exportStar(require("./language"), exports);
18
18
  __exportStar(require("./login"), exports);
19
19
  __exportStar(require("./package"), exports);
20
20
  __exportStar(require("./plan"), exports);
21
+ __exportStar(require("./protocol"), exports);
22
+ __exportStar(require("./signup"), exports);
21
23
  __exportStar(require("./token"), exports);
22
24
  const protocol_1 = __importDefault(require("./protocol"));
23
- __exportStar(require("./protocol"), exports);
24
25
  exports.default = protocol_1.default;
@@ -1,13 +1,23 @@
1
1
  import { LanguageId } from './deployment';
2
+ export declare type Runner = 'nodejs' | 'python' | 'ruby' | 'csharp';
3
+ export interface RunnerInfo {
4
+ id: Runner;
5
+ languageId: LanguageId;
6
+ filePatterns: RegExp[];
7
+ installCommand: string;
8
+ displayName: string;
9
+ }
10
+ export declare const Runners: Record<Runner, RunnerInfo>;
2
11
  interface Language {
3
12
  tag: string;
4
13
  displayName: string;
5
14
  hexColor: string;
6
15
  fileExtRegex: RegExp;
7
- runnerName?: string;
16
+ runnerName?: Runner;
8
17
  runnerFilesRegexes: RegExp[];
9
18
  }
10
19
  export declare const Languages: Record<LanguageId, Language>;
11
20
  export declare const DisplayNameToLanguageId: Record<string, LanguageId>;
12
21
  export declare const RunnerToDisplayName: (runner: string) => string;
22
+ export declare const detectRunnersFromFiles: (files: string[]) => Runner[];
13
23
  export {};
package/dist/language.js CHANGED
@@ -6,7 +6,38 @@
6
6
 
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.RunnerToDisplayName = exports.DisplayNameToLanguageId = exports.Languages = void 0;
9
+ exports.detectRunnersFromFiles = exports.RunnerToDisplayName = exports.DisplayNameToLanguageId = exports.Languages = exports.Runners = void 0;
10
+ const path_1 = require("path");
11
+ exports.Runners = {
12
+ nodejs: {
13
+ id: 'nodejs',
14
+ languageId: 'node',
15
+ filePatterns: [/^package\.json$/],
16
+ installCommand: 'npm install',
17
+ displayName: 'NPM'
18
+ },
19
+ python: {
20
+ id: 'python',
21
+ languageId: 'py',
22
+ filePatterns: [/^requirements\.txt$/],
23
+ installCommand: 'pip install -r requirements.txt',
24
+ displayName: 'Pip'
25
+ },
26
+ ruby: {
27
+ id: 'ruby',
28
+ languageId: 'rb',
29
+ filePatterns: [/^Gemfile$/],
30
+ installCommand: 'bundle install',
31
+ displayName: 'Gem'
32
+ },
33
+ csharp: {
34
+ id: 'csharp',
35
+ languageId: 'cs',
36
+ filePatterns: [/^project\.json$/, /\.csproj$/],
37
+ installCommand: 'dotnet restore',
38
+ displayName: 'NuGet'
39
+ }
40
+ };
10
41
  exports.Languages = {
11
42
  cs: {
12
43
  tag: 'cs',
@@ -14,7 +45,7 @@ exports.Languages = {
14
45
  hexColor: '#953dac',
15
46
  fileExtRegex: /^cs$/,
16
47
  runnerName: 'csharp',
17
- runnerFilesRegexes: [/^project\.json$/, /\.csproj$/]
48
+ runnerFilesRegexes: exports.Runners.csharp.filePatterns
18
49
  },
19
50
  py: {
20
51
  tag: 'py',
@@ -22,7 +53,7 @@ exports.Languages = {
22
53
  hexColor: '#ffd43b',
23
54
  fileExtRegex: /^py$/,
24
55
  runnerName: 'python',
25
- runnerFilesRegexes: [/^requirements\.txt$/]
56
+ runnerFilesRegexes: exports.Runners.python.filePatterns
26
57
  },
27
58
  rb: {
28
59
  tag: 'rb',
@@ -30,7 +61,7 @@ exports.Languages = {
30
61
  hexColor: '#e53935',
31
62
  fileExtRegex: /^rb$/,
32
63
  runnerName: 'ruby',
33
- runnerFilesRegexes: [/^Gemfile$/]
64
+ runnerFilesRegexes: exports.Runners.ruby.filePatterns
34
65
  },
35
66
  node: {
36
67
  tag: 'node',
@@ -38,7 +69,7 @@ exports.Languages = {
38
69
  hexColor: '#3c873a',
39
70
  fileExtRegex: /^js$/,
40
71
  runnerName: 'nodejs',
41
- runnerFilesRegexes: [/^package\.json$/]
72
+ runnerFilesRegexes: exports.Runners.nodejs.filePatterns
42
73
  },
43
74
  ts: {
44
75
  tag: 'ts',
@@ -46,7 +77,7 @@ exports.Languages = {
46
77
  hexColor: '#007acc',
47
78
  fileExtRegex: /^(ts|tsx)$/,
48
79
  runnerName: 'nodejs',
49
- runnerFilesRegexes: [/^package\.json$/] // TODO: Use tsconfig instead?
80
+ runnerFilesRegexes: exports.Runners.nodejs.filePatterns
50
81
  },
51
82
  file: {
52
83
  tag: 'file',
@@ -77,12 +108,23 @@ exports.DisplayNameToLanguageId = Object.keys(exports.Languages).reduce((obj, la
77
108
  [exports.Languages[lang].displayName]: lang
78
109
  }), {});
79
110
  const RunnerToDisplayName = (runner) => {
80
- const displayNameMap = {
81
- nodejs: 'NPM',
82
- python: 'Pip',
83
- ruby: 'Gem',
84
- csharp: 'NuGet'
85
- };
86
- return displayNameMap[runner] || 'Build';
111
+ const match = exports.Runners[runner];
112
+ return match ? match.displayName : 'Build';
87
113
  };
88
114
  exports.RunnerToDisplayName = RunnerToDisplayName;
115
+ const detectRunnersFromFiles = (files) => {
116
+ const runners = new Set();
117
+ for (const file of files) {
118
+ const fileName = path_1.basename(file);
119
+ for (const runner of Object.values(exports.Runners)) {
120
+ for (const pattern of runner.filePatterns) {
121
+ if (pattern.exec(fileName)) {
122
+ runners.add(runner.id);
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return Array.from(runners);
129
+ };
130
+ exports.detectRunnersFromFiles = detectRunnersFromFiles;
package/dist/package.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { MetaCallJSON } from './deployment';
2
+ import { Runner } from './language';
2
3
  export declare const findFilesPath: (path?: string, ignoreFiles?: string[]) => Promise<string[]>;
3
4
  export declare const pathIsMetaCallJson: (path: string) => boolean;
4
5
  export declare const findMetaCallJsons: (files: string[]) => string[];
5
- export declare const findRunners: (files: string[]) => Set<string>;
6
+ export declare const findRunners: (files: string[]) => Set<Runner>;
7
+ export declare const detectRunners: (path?: string, ignoreFiles?: string[]) => Promise<Runner[]>;
6
8
  export declare enum PackageError {
7
9
  Empty = "No files found in the current folder",
8
10
  JsonNotFound = "No metacall.json found in the current folder",
@@ -12,7 +14,7 @@ interface PackageDescriptor {
12
14
  error: PackageError;
13
15
  files: string[];
14
16
  jsons: string[];
15
- runners: string[];
17
+ runners: Runner[];
16
18
  }
17
19
  export declare const generatePackage: (path?: string) => Promise<PackageDescriptor>;
18
20
  export declare const generateJsonsFromFiles: (files: string[]) => MetaCallJSON[];
package/dist/package.js CHANGED
@@ -16,7 +16,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  return (mod && mod.__esModule) ? mod : { "default": mod };
17
17
  };
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.generateJsonsFromFiles = exports.generatePackage = exports.PackageError = exports.findRunners = exports.findMetaCallJsons = exports.pathIsMetaCallJson = exports.findFilesPath = void 0;
19
+ exports.generateJsonsFromFiles = exports.generatePackage = exports.PackageError = exports.detectRunners = exports.findRunners = exports.findMetaCallJsons = exports.pathIsMetaCallJson = exports.findFilesPath = void 0;
20
20
  const ignore_walk_1 = __importDefault(require("ignore-walk"));
21
21
  const path_1 = require("path");
22
22
  const language_1 = require("./language");
@@ -31,22 +31,10 @@ const pathIsMetaCallJson = (path) => !!/^metacall(-.+)?\.json$/.exec(path_1.base
31
31
  exports.pathIsMetaCallJson = pathIsMetaCallJson;
32
32
  const findMetaCallJsons = (files) => files.filter(exports.pathIsMetaCallJson);
33
33
  exports.findMetaCallJsons = findMetaCallJsons;
34
- const findRunners = (files) => {
35
- const runners = new Set();
36
- for (const file of files) {
37
- const fileName = path_1.basename(file);
38
- for (const langId of Object.keys(language_1.Languages)) {
39
- const lang = language_1.Languages[langId];
40
- for (const re of lang.runnerFilesRegexes) {
41
- if (re.exec(fileName) && lang.runnerName) {
42
- runners.add(lang.runnerName);
43
- }
44
- }
45
- }
46
- }
47
- return runners;
48
- };
34
+ const findRunners = (files) => new Set(language_1.detectRunnersFromFiles(files));
49
35
  exports.findRunners = findRunners;
36
+ const detectRunners = async (path = process.cwd(), ignoreFiles = ['.gitignore']) => language_1.detectRunnersFromFiles(await exports.findFilesPath(path, ignoreFiles));
37
+ exports.detectRunners = detectRunners;
50
38
  var PackageError;
51
39
  (function (PackageError) {
52
40
  PackageError["Empty"] = "No files found in the current folder";
@@ -1,6 +1,11 @@
1
1
  import { AxiosError } from 'axios';
2
2
  import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
3
3
  import { Plans } from './plan';
4
+ /**
5
+ * Type guard for protocol-specific errors (Axios errors in this case).
6
+ * @param err - The unknown error to check.
7
+ * @returns True if the error is an ProtocolError, false otherwise.
8
+ */
4
9
  export declare const isProtocolError: (err: unknown) => boolean;
5
10
  export { AxiosError as ProtocolError };
6
11
  declare type SubscriptionMap = Record<string, number>;
@@ -20,13 +25,47 @@ export interface AddResponse {
20
25
  export interface Branches {
21
26
  branches: [string];
22
27
  }
28
+ export declare enum InvokeType {
29
+ Call = "call",
30
+ Await = "await"
31
+ }
32
+ export interface DeployCreateRequest {
33
+ suffix: string;
34
+ resourceType: ResourceType;
35
+ release: string;
36
+ env: {
37
+ name: string;
38
+ value: string;
39
+ }[];
40
+ plan: Plans;
41
+ version: string;
42
+ }
43
+ export interface DeployDeleteRequest {
44
+ prefix: string;
45
+ suffix: string;
46
+ version: string;
47
+ }
48
+ export interface RepositoryAddRequest {
49
+ url: string;
50
+ branch: string;
51
+ jsons: MetaCallJSON[];
52
+ }
53
+ export interface RepositoryBranchListRequest {
54
+ url: string;
55
+ }
56
+ export interface RepositoryFileListRequest {
57
+ url: string;
58
+ branch: string;
59
+ }
23
60
  export interface API {
24
61
  refresh(): Promise<string>;
62
+ ready(): Promise<boolean>;
25
63
  validate(): Promise<boolean>;
26
64
  deployEnabled(): Promise<boolean>;
27
65
  listSubscriptions(): Promise<SubscriptionMap>;
28
66
  listSubscriptionsDeploys(): Promise<SubscriptionDeploy[]>;
29
67
  inspect(): Promise<Deployment[]>;
68
+ inspectByName(suffix: string): Promise<Deployment>;
30
69
  upload(name: string, blob: unknown, jsons?: MetaCallJSON[], runners?: string[]): Promise<string>;
31
70
  add(url: string, branch: string, jsons: MetaCallJSON[]): Promise<AddResponse>;
32
71
  deploy(name: string, env: {
@@ -37,6 +76,50 @@ export interface API {
37
76
  logs(container: string, type: LogType, suffix: string, prefix: string, version?: string): Promise<string>;
38
77
  branchList(url: string): Promise<Branches>;
39
78
  fileList(url: string, branch: string): Promise<string[]>;
79
+ invoke<Result, Args = unknown>(type: InvokeType, prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
80
+ call<Result, Args = unknown>(prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
81
+ await<Result, Args = unknown>(prefix: string, suffix: string, version: string, name: string, args?: Args): Promise<Result>;
40
82
  }
41
83
  declare const _default: (token: string, baseURL: string) => API;
42
84
  export default _default;
85
+ export declare const MaxRetries = 30;
86
+ export declare const MaxRetryInterval = 2000;
87
+ export declare const MaxFuncLength = 64;
88
+ /**
89
+ * Executes an asynchronous function with automatic retry logic.
90
+ *
91
+ * The function will be retried up to `maxRetries` times, waiting `interval`
92
+ * milliseconds between each attempt. If all retries fail, the last error is
93
+ * wrapped in a new `Error` with a descriptive message, including:
94
+ * - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
95
+ * - Number of retries attempted
96
+ * - Original error message
97
+ *
98
+ * Error handling is fully type-safe:
99
+ * - If the error is an ProtocolError (checked via `isProtocolError`), its
100
+ * message is used.
101
+ * - If the error is a standard `Error`, its `message` is used.
102
+ * - Otherwise, the error is converted to a string.
103
+ *
104
+ * @typeParam T - The return type of the function being retried.
105
+ * @param fn - A lambda or bound function returning a `Promise<T>`. The
106
+ * function should contain the logic you want to retry.
107
+ * @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
108
+ * @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
109
+ * @returns A `Promise` resolving to the return value of `fn` if successful.
110
+ * @throws Error If all retry attempts fail, throws a new Error containing
111
+ * information about the function and the last error.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const deployment = await waitFor(() => api.inspectByName('my-suffix'));
116
+ * ```
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const result = await waitFor(
121
+ * () => api.deploy(name, env, plan, resourceType)
122
+ * );
123
+ * ```
124
+ */
125
+ export declare const waitFor: <T>(fn: () => Promise<T>, maxRetries?: number, interval?: number) => Promise<T>;
package/dist/protocol.js CHANGED
@@ -18,14 +18,40 @@
18
18
  branchList: get the branches of a repository
19
19
  fileList: get files of a repository by branch
20
20
  */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
21
40
  var __importDefault = (this && this.__importDefault) || function (mod) {
22
41
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
42
  };
24
43
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.ResourceType = exports.isProtocolError = void 0;
26
- const axios_1 = __importDefault(require("axios"));
44
+ exports.waitFor = exports.MaxFuncLength = exports.MaxRetryInterval = exports.MaxRetries = exports.InvokeType = exports.ResourceType = exports.ProtocolError = exports.isProtocolError = void 0;
45
+ const axios_1 = __importStar(require("axios"));
46
+ Object.defineProperty(exports, "ProtocolError", { enumerable: true, get: function () { return axios_1.AxiosError; } });
27
47
  const form_data_1 = __importDefault(require("form-data"));
48
+ const url_1 = require("url");
28
49
  const deployment_1 = require("./deployment");
50
+ /**
51
+ * Type guard for protocol-specific errors (Axios errors in this case).
52
+ * @param err - The unknown error to check.
53
+ * @returns True if the error is an ProtocolError, false otherwise.
54
+ */
29
55
  const isProtocolError = (err) => axios_1.default.isAxiosError(err);
30
56
  exports.isProtocolError = isProtocolError;
31
57
  var ResourceType;
@@ -33,27 +59,36 @@ var ResourceType;
33
59
  ResourceType["Package"] = "Package";
34
60
  ResourceType["Repository"] = "Repository";
35
61
  })(ResourceType = exports.ResourceType || (exports.ResourceType = {}));
62
+ var InvokeType;
63
+ (function (InvokeType) {
64
+ InvokeType["Call"] = "call";
65
+ InvokeType["Await"] = "await";
66
+ })(InvokeType = exports.InvokeType || (exports.InvokeType = {}));
36
67
  exports.default = (token, baseURL) => {
68
+ const getURL = (path) => new url_1.URL(path, baseURL).toString();
69
+ const getConfig = (headers = {}) => {
70
+ return {
71
+ headers: {
72
+ Authorization: 'jwt ' + token,
73
+ ...headers
74
+ }
75
+ };
76
+ };
37
77
  const api = {
38
78
  refresh: () => axios_1.default
39
- .get(baseURL + '/api/account/refresh-token', {
40
- headers: { Authorization: 'jwt ' + token }
41
- })
79
+ .get(getURL('/api/account/refresh-token'), getConfig())
42
80
  .then(res => res.data),
81
+ ready: () => axios_1.default
82
+ .get(getURL('/api/readiness'), getConfig())
83
+ .then(res => res.status == 200),
43
84
  validate: () => axios_1.default
44
- .get(baseURL + '/validate', {
45
- headers: { Authorization: 'jwt ' + token }
46
- })
85
+ .get(getURL('/validate'), getConfig())
47
86
  .then(res => res.data),
48
87
  deployEnabled: () => axios_1.default
49
- .get(baseURL + '/api/account/deploy-enabled', {
50
- headers: { Authorization: 'jwt ' + token }
51
- })
88
+ .get(getURL('/api/account/deploy-enabled'), getConfig())
52
89
  .then(res => res.data),
53
90
  listSubscriptions: async () => {
54
- const res = await axios_1.default.get(baseURL + '/api/billing/list-subscriptions', {
55
- headers: { Authorization: 'jwt ' + token }
56
- });
91
+ const res = await axios_1.default.get(getURL('/api/billing/list-subscriptions'), getConfig());
57
92
  const subscriptions = {};
58
93
  for (const id of res.data) {
59
94
  if (subscriptions[id] === undefined) {
@@ -65,18 +100,21 @@ exports.default = (token, baseURL) => {
65
100
  }
66
101
  return subscriptions;
67
102
  },
68
- listSubscriptionsDeploys: async () => axios_1.default
69
- .get(baseURL + '/api/billing/list-subscriptions-deploys', {
70
- headers: { Authorization: 'jwt ' + token }
71
- })
103
+ listSubscriptionsDeploys: () => axios_1.default
104
+ .get(getURL('/api/billing/list-subscriptions-deploys'), getConfig())
72
105
  .then(res => res.data),
73
- inspect: async () => axios_1.default
74
- .get(baseURL + '/api/inspect', {
75
- headers: { Authorization: 'jwt ' + token }
76
- })
106
+ inspect: () => axios_1.default
107
+ .get(getURL('/api/inspect'), getConfig())
77
108
  .then(res => res.data),
109
+ inspectByName: async (suffix) => {
110
+ const deployments = await api.inspect();
111
+ const deploy = deployments.find(deploy => deploy.suffix == suffix);
112
+ if (!deploy) {
113
+ throw new Error(`Deployment with suffix '${suffix}' not found`);
114
+ }
115
+ return deploy;
116
+ },
78
117
  upload: async (name, blob, jsons = [], runners = []) => {
79
- var _a, _b;
80
118
  const fd = new form_data_1.default();
81
119
  fd.append('id', name);
82
120
  fd.append('type', 'application/x-zip-compressed');
@@ -86,70 +124,130 @@ exports.default = (token, baseURL) => {
86
124
  filename: 'blob',
87
125
  contentType: 'application/x-zip-compressed'
88
126
  });
89
- const res = await axios_1.default.post(baseURL + '/api/package/create', fd, {
90
- headers: {
91
- Authorization: 'jwt ' + token,
92
- ...((_b = (_a = fd.getHeaders) === null || _a === void 0 ? void 0 : _a.call(fd)) !== null && _b !== void 0 ? _b : {}) // operator chaining to make it compatible with frontend
93
- }
94
- });
127
+ const res = await axios_1.default.post(getURL('/api/package/create'), fd, getConfig() // Axios automatically sets multipart headers
128
+ );
95
129
  return res.data;
96
130
  },
97
131
  add: (url, branch, jsons = []) => axios_1.default
98
- .post(baseURL + '/api/repository/add', {
132
+ .post(getURL('/api/repository/add'), {
99
133
  url,
100
134
  branch,
101
135
  jsons
102
- }, {
103
- headers: { Authorization: 'jwt ' + token }
104
- })
105
- .then((res) => res.data),
136
+ }, getConfig())
137
+ .then(res => res.data),
106
138
  branchList: (url) => axios_1.default
107
- .post(baseURL + '/api/repository/branchlist', {
139
+ .post(getURL('/api/repository/branchlist'), {
108
140
  url
109
- }, {
110
- headers: { Authorization: 'jwt ' + token }
111
- })
112
- .then((res) => res.data),
141
+ }, getConfig())
142
+ .then(res => res.data),
113
143
  deploy: (name, env, plan, resourceType, release = Date.now().toString(16), version = 'v1') => axios_1.default
114
- .post(baseURL + '/api/deploy/create', {
144
+ .post(getURL('/api/deploy/create'), {
115
145
  resourceType,
116
146
  suffix: name,
117
147
  release,
118
148
  env,
119
149
  plan,
120
150
  version
121
- }, {
122
- headers: { Authorization: 'jwt ' + token }
123
- })
151
+ }, getConfig())
124
152
  .then(res => res.data),
125
153
  deployDelete: (prefix, suffix, version = 'v1') => axios_1.default
126
- .post(baseURL + '/api/deploy/delete', {
154
+ .post(getURL('/api/deploy/delete'), {
127
155
  prefix,
128
156
  suffix,
129
157
  version
130
- }, {
131
- headers: { Authorization: 'jwt ' + token }
132
- })
158
+ }, getConfig())
133
159
  .then(res => res.data),
134
160
  logs: (container, type = deployment_1.LogType.Deploy, suffix, prefix, version = 'v1') => axios_1.default
135
- .post(baseURL + '/api/deploy/logs', {
161
+ .post(getURL('/api/deploy/logs'), {
136
162
  container,
137
163
  type,
138
164
  suffix,
139
165
  prefix,
140
166
  version
141
- }, {
142
- headers: { Authorization: 'jwt ' + token }
143
- })
167
+ }, getConfig())
144
168
  .then(res => res.data),
145
169
  fileList: (url, branch) => axios_1.default
146
- .post(baseURL + '/api/repository/filelist', {
170
+ .post(getURL('/api/repository/filelist'), {
147
171
  url,
148
172
  branch
149
- }, {
150
- headers: { Authorization: 'jwt ' + token }
151
- })
152
- .then(res => res.data['files'])
173
+ }, getConfig())
174
+ .then(res => res.data['files']),
175
+ invoke: (type, prefix, suffix, version = 'v1', name, args) => {
176
+ const url = getURL(`/${prefix}/${suffix}/${version}/${type}/${name}`);
177
+ const config = getConfig();
178
+ const req = args === undefined
179
+ ? axios_1.default.get(url, config)
180
+ : axios_1.default.post(url, args, config);
181
+ return req.then(res => res.data);
182
+ },
183
+ call: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Call, prefix, suffix, version, name, args),
184
+ await: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Await, prefix, suffix, version, name, args)
153
185
  };
154
186
  return api;
155
187
  };
188
+ exports.MaxRetries = 30;
189
+ exports.MaxRetryInterval = 2000;
190
+ exports.MaxFuncLength = 64;
191
+ /**
192
+ * Executes an asynchronous function with automatic retry logic.
193
+ *
194
+ * The function will be retried up to `maxRetries` times, waiting `interval`
195
+ * milliseconds between each attempt. If all retries fail, the last error is
196
+ * wrapped in a new `Error` with a descriptive message, including:
197
+ * - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
198
+ * - Number of retries attempted
199
+ * - Original error message
200
+ *
201
+ * Error handling is fully type-safe:
202
+ * - If the error is an ProtocolError (checked via `isProtocolError`), its
203
+ * message is used.
204
+ * - If the error is a standard `Error`, its `message` is used.
205
+ * - Otherwise, the error is converted to a string.
206
+ *
207
+ * @typeParam T - The return type of the function being retried.
208
+ * @param fn - A lambda or bound function returning a `Promise<T>`. The
209
+ * function should contain the logic you want to retry.
210
+ * @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
211
+ * @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
212
+ * @returns A `Promise` resolving to the return value of `fn` if successful.
213
+ * @throws Error If all retry attempts fail, throws a new Error containing
214
+ * information about the function and the last error.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const deployment = await waitFor(() => api.inspectByName('my-suffix'));
219
+ * ```
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const result = await waitFor(
224
+ * () => api.deploy(name, env, plan, resourceType)
225
+ * );
226
+ * ```
227
+ */
228
+ const waitFor = async (fn, maxRetries = exports.MaxRetries, interval = exports.MaxRetryInterval) => {
229
+ let retry = 0;
230
+ for (;;) {
231
+ try {
232
+ return await fn();
233
+ }
234
+ catch (error) {
235
+ retry++;
236
+ if (retry >= maxRetries) {
237
+ const fnStr = fn.toString();
238
+ const func = fn.name ||
239
+ (fnStr.length > exports.MaxFuncLength
240
+ ? fnStr.slice(0, exports.MaxFuncLength) + '...'
241
+ : fnStr);
242
+ const message = exports.isProtocolError(error)
243
+ ? error.message
244
+ : error instanceof Error
245
+ ? error.message
246
+ : String(error);
247
+ throw new Error(`Failed to execute '${func}' after ${maxRetries} retries: ${message}`);
248
+ }
249
+ await new Promise(r => setTimeout(r, interval));
250
+ }
251
+ }
252
+ };
253
+ exports.waitFor = waitFor;
package/dist/token.js CHANGED
@@ -11,6 +11,6 @@ const expiresIn = (token) => {
11
11
  return 0;
12
12
  }
13
13
  const now = Date.now() / 1000;
14
- return new Date(((decoded === null || decoded === void 0 ? void 0 : decoded['exp']) || now) * 1000).getTime() - now * 1000;
14
+ return new Date((decoded?.['exp'] || now) * 1000).getTime() - now * 1000;
15
15
  };
16
16
  exports.expiresIn = expiresIn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metacall/protocol",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Tool for deploying into MetaCall FaaS platform.",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js",
@@ -85,7 +85,7 @@
85
85
  }
86
86
  },
87
87
  "dependencies": {
88
- "axios": "^0.21.0",
88
+ "axios": "^1.13.5",
89
89
  "form-data": "^3.0.0",
90
90
  "ignore-walk": "^3.0.4",
91
91
  "jsonwebtoken": "^9.0.0"
@@ -111,4 +111,4 @@
111
111
  "typescript": "4.3.2",
112
112
  "yamljs": "^0.3.0"
113
113
  }
114
- }
114
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,9 @@ export * from './language';
3
3
  export * from './login';
4
4
  export * from './package';
5
5
  export * from './plan';
6
+ export * from './protocol';
7
+ export * from './signup';
6
8
  export * from './token';
9
+
7
10
  import metacallAPI from './protocol';
8
- export * from './protocol';
9
11
  export default metacallAPI;
package/src/language.ts CHANGED
@@ -5,14 +5,56 @@
5
5
 
6
6
  */
7
7
 
8
+ import { basename } from 'path';
8
9
  import { LanguageId } from './deployment';
9
10
 
11
+ export type Runner = 'nodejs' | 'python' | 'ruby' | 'csharp';
12
+
13
+ export interface RunnerInfo {
14
+ id: Runner;
15
+ languageId: LanguageId;
16
+ filePatterns: RegExp[];
17
+ installCommand: string;
18
+ displayName: string;
19
+ }
20
+
21
+ export const Runners: Record<Runner, RunnerInfo> = {
22
+ nodejs: {
23
+ id: 'nodejs',
24
+ languageId: 'node',
25
+ filePatterns: [/^package\.json$/],
26
+ installCommand: 'npm install',
27
+ displayName: 'NPM'
28
+ },
29
+ python: {
30
+ id: 'python',
31
+ languageId: 'py',
32
+ filePatterns: [/^requirements\.txt$/],
33
+ installCommand: 'pip install -r requirements.txt',
34
+ displayName: 'Pip'
35
+ },
36
+ ruby: {
37
+ id: 'ruby',
38
+ languageId: 'rb',
39
+ filePatterns: [/^Gemfile$/],
40
+ installCommand: 'bundle install',
41
+ displayName: 'Gem'
42
+ },
43
+ csharp: {
44
+ id: 'csharp',
45
+ languageId: 'cs',
46
+ filePatterns: [/^project\.json$/, /\.csproj$/],
47
+ installCommand: 'dotnet restore',
48
+ displayName: 'NuGet'
49
+ }
50
+ };
51
+
10
52
  interface Language {
11
53
  tag: string; // Tag which corresponds to language_id in metacall.json
12
54
  displayName: string; // Name for displaying the language
13
55
  hexColor: string; // Color for displaying the language related things
14
56
  fileExtRegex: RegExp; // Regex for selecting the metacall.json scripts field
15
- runnerName?: string; // Id of the runner
57
+ runnerName?: Runner; // Id of the runner
16
58
  runnerFilesRegexes: RegExp[]; // Regex for generating the runners list
17
59
  }
18
60
 
@@ -23,7 +65,7 @@ export const Languages: Record<LanguageId, Language> = {
23
65
  hexColor: '#953dac',
24
66
  fileExtRegex: /^cs$/,
25
67
  runnerName: 'csharp',
26
- runnerFilesRegexes: [/^project\.json$/, /\.csproj$/]
68
+ runnerFilesRegexes: Runners.csharp.filePatterns
27
69
  },
28
70
  py: {
29
71
  tag: 'py',
@@ -31,7 +73,7 @@ export const Languages: Record<LanguageId, Language> = {
31
73
  hexColor: '#ffd43b',
32
74
  fileExtRegex: /^py$/,
33
75
  runnerName: 'python',
34
- runnerFilesRegexes: [/^requirements\.txt$/]
76
+ runnerFilesRegexes: Runners.python.filePatterns
35
77
  },
36
78
  rb: {
37
79
  tag: 'rb',
@@ -39,7 +81,7 @@ export const Languages: Record<LanguageId, Language> = {
39
81
  hexColor: '#e53935',
40
82
  fileExtRegex: /^rb$/,
41
83
  runnerName: 'ruby',
42
- runnerFilesRegexes: [/^Gemfile$/]
84
+ runnerFilesRegexes: Runners.ruby.filePatterns
43
85
  },
44
86
  node: {
45
87
  tag: 'node',
@@ -47,7 +89,7 @@ export const Languages: Record<LanguageId, Language> = {
47
89
  hexColor: '#3c873a',
48
90
  fileExtRegex: /^js$/,
49
91
  runnerName: 'nodejs',
50
- runnerFilesRegexes: [/^package\.json$/]
92
+ runnerFilesRegexes: Runners.nodejs.filePatterns
51
93
  },
52
94
  ts: {
53
95
  tag: 'ts',
@@ -55,7 +97,7 @@ export const Languages: Record<LanguageId, Language> = {
55
97
  hexColor: '#007acc',
56
98
  fileExtRegex: /^(ts|tsx)$/,
57
99
  runnerName: 'nodejs',
58
- runnerFilesRegexes: [/^package\.json$/] // TODO: Use tsconfig instead?
100
+ runnerFilesRegexes: Runners.nodejs.filePatterns
59
101
  },
60
102
  file: {
61
103
  tag: 'file',
@@ -94,12 +136,26 @@ export const DisplayNameToLanguageId: Record<string, LanguageId> = Object.keys(
94
136
  );
95
137
 
96
138
  export const RunnerToDisplayName = (runner: string): string => {
97
- const displayNameMap: Record<string, string> = {
98
- nodejs: 'NPM',
99
- python: 'Pip',
100
- ruby: 'Gem',
101
- csharp: 'NuGet'
102
- };
139
+ const match = Runners[runner as Runner];
140
+
141
+ return match ? match.displayName : 'Build';
142
+ };
143
+
144
+ export const detectRunnersFromFiles = (files: string[]): Runner[] => {
145
+ const runners = new Set<Runner>();
146
+
147
+ for (const file of files) {
148
+ const fileName = basename(file);
149
+
150
+ for (const runner of Object.values(Runners)) {
151
+ for (const pattern of runner.filePatterns) {
152
+ if (pattern.exec(fileName)) {
153
+ runners.add(runner.id);
154
+ break;
155
+ }
156
+ }
157
+ }
158
+ }
103
159
 
104
- return displayNameMap[runner] || 'Build';
160
+ return Array.from(runners);
105
161
  };
package/src/package.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  import walk from 'ignore-walk';
16
16
  import { basename, extname } from 'path';
17
17
  import { LanguageId, MetaCallJSON } from './deployment';
18
- import { Languages } from './language';
18
+ import { detectRunnersFromFiles, Languages, Runner } from './language';
19
19
 
20
20
  export const findFilesPath = async (
21
21
  path: string = process.cwd(),
@@ -36,23 +36,14 @@ export const pathIsMetaCallJson = (path: string): boolean =>
36
36
  export const findMetaCallJsons = (files: string[]): string[] =>
37
37
  files.filter(pathIsMetaCallJson);
38
38
 
39
- export const findRunners = (files: string[]): Set<string> => {
40
- const runners: Set<string> = new Set<string>();
41
-
42
- for (const file of files) {
43
- const fileName = basename(file);
44
- for (const langId of Object.keys(Languages)) {
45
- const lang = Languages[langId as LanguageId];
46
- for (const re of lang.runnerFilesRegexes) {
47
- if (re.exec(fileName) && lang.runnerName) {
48
- runners.add(lang.runnerName);
49
- }
50
- }
51
- }
52
- }
39
+ export const findRunners = (files: string[]): Set<Runner> =>
40
+ new Set<Runner>(detectRunnersFromFiles(files));
53
41
 
54
- return runners;
55
- };
42
+ export const detectRunners = async (
43
+ path: string = process.cwd(),
44
+ ignoreFiles: string[] = ['.gitignore']
45
+ ): Promise<Runner[]> =>
46
+ detectRunnersFromFiles(await findFilesPath(path, ignoreFiles));
56
47
 
57
48
  export enum PackageError {
58
49
  Empty = 'No files found in the current folder',
@@ -64,7 +55,7 @@ interface PackageDescriptor {
64
55
  error: PackageError;
65
56
  files: string[];
66
57
  jsons: string[];
67
- runners: string[];
58
+ runners: Runner[];
68
59
  }
69
60
 
70
61
  const NullPackage: PackageDescriptor = {
package/src/protocol.ts CHANGED
@@ -18,11 +18,18 @@
18
18
  fileList: get files of a repository by branch
19
19
  */
20
20
 
21
- import axios, { AxiosError, AxiosResponse } from 'axios';
21
+ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
22
22
  import FormData from 'form-data';
23
+ import { URL } from 'url';
23
24
  import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
24
25
  import { Plans } from './plan';
26
+ import { ProtocolError } from './protocol';
25
27
 
28
+ /**
29
+ * Type guard for protocol-specific errors (Axios errors in this case).
30
+ * @param err - The unknown error to check.
31
+ * @returns True if the error is an ProtocolError, false otherwise.
32
+ */
26
33
  export const isProtocolError = (err: unknown): boolean =>
27
34
  axios.isAxiosError(err);
28
35
 
@@ -50,13 +57,50 @@ export interface Branches {
50
57
  branches: [string];
51
58
  }
52
59
 
60
+ export enum InvokeType {
61
+ Call = 'call',
62
+ Await = 'await'
63
+ }
64
+
65
+ export interface DeployCreateRequest {
66
+ suffix: string;
67
+ resourceType: ResourceType;
68
+ release: string;
69
+ env: { name: string; value: string }[];
70
+ plan: Plans;
71
+ version: string;
72
+ }
73
+
74
+ export interface DeployDeleteRequest {
75
+ prefix: string;
76
+ suffix: string;
77
+ version: string;
78
+ }
79
+
80
+ export interface RepositoryAddRequest {
81
+ url: string;
82
+ branch: string;
83
+ jsons: MetaCallJSON[];
84
+ }
85
+
86
+ export interface RepositoryBranchListRequest {
87
+ url: string;
88
+ }
89
+
90
+ export interface RepositoryFileListRequest {
91
+ url: string;
92
+ branch: string;
93
+ }
94
+
53
95
  export interface API {
54
96
  refresh(): Promise<string>;
97
+ ready(): Promise<boolean>;
55
98
  validate(): Promise<boolean>;
56
99
  deployEnabled(): Promise<boolean>;
57
100
  listSubscriptions(): Promise<SubscriptionMap>;
58
101
  listSubscriptionsDeploys(): Promise<SubscriptionDeploy[]>;
59
102
  inspect(): Promise<Deployment[]>;
103
+ inspectByName(suffix: string): Promise<Deployment>;
60
104
  upload(
61
105
  name: string,
62
106
  blob: unknown,
@@ -90,37 +134,69 @@ export interface API {
90
134
  ): Promise<string>;
91
135
  branchList(url: string): Promise<Branches>;
92
136
  fileList(url: string, branch: string): Promise<string[]>;
137
+ invoke<Result, Args = unknown>(
138
+ type: InvokeType,
139
+ prefix: string,
140
+ suffix: string,
141
+ version: string,
142
+ name: string,
143
+ args?: Args
144
+ ): Promise<Result>;
145
+ call<Result, Args = unknown>(
146
+ prefix: string,
147
+ suffix: string,
148
+ version: string,
149
+ name: string,
150
+ args?: Args
151
+ ): Promise<Result>;
152
+ await<Result, Args = unknown>(
153
+ prefix: string,
154
+ suffix: string,
155
+ version: string,
156
+ name: string,
157
+ args?: Args
158
+ ): Promise<Result>;
93
159
  }
94
160
 
95
161
  export default (token: string, baseURL: string): API => {
162
+ const getURL = (path: string): string => new URL(path, baseURL).toString();
163
+ const getConfig = (headers = {}): AxiosRequestConfig => {
164
+ return {
165
+ headers: {
166
+ Authorization: 'jwt ' + token,
167
+ ...headers
168
+ }
169
+ };
170
+ };
171
+
96
172
  const api: API = {
97
173
  refresh: (): Promise<string> =>
98
174
  axios
99
- .get<string>(baseURL + '/api/account/refresh-token', {
100
- headers: { Authorization: 'jwt ' + token }
101
- })
175
+ .get<string>(getURL('/api/account/refresh-token'), getConfig())
102
176
  .then(res => res.data),
103
177
 
178
+ ready: (): Promise<boolean> =>
179
+ axios
180
+ .get<boolean>(getURL('/api/readiness'), getConfig())
181
+ .then(res => res.status == 200),
182
+
104
183
  validate: (): Promise<boolean> =>
105
184
  axios
106
- .get<boolean>(baseURL + '/validate', {
107
- headers: { Authorization: 'jwt ' + token }
108
- })
185
+ .get<boolean>(getURL('/validate'), getConfig())
109
186
  .then(res => res.data),
110
187
 
111
188
  deployEnabled: (): Promise<boolean> =>
112
189
  axios
113
- .get<boolean>(baseURL + '/api/account/deploy-enabled', {
114
- headers: { Authorization: 'jwt ' + token }
115
- })
190
+ .get<boolean>(
191
+ getURL('/api/account/deploy-enabled'),
192
+ getConfig()
193
+ )
116
194
  .then(res => res.data),
117
195
 
118
196
  listSubscriptions: async (): Promise<SubscriptionMap> => {
119
197
  const res = await axios.get<string[]>(
120
- baseURL + '/api/billing/list-subscriptions',
121
- {
122
- headers: { Authorization: 'jwt ' + token }
123
- }
198
+ getURL('/api/billing/list-subscriptions'),
199
+ getConfig()
124
200
  );
125
201
 
126
202
  const subscriptions: SubscriptionMap = {};
@@ -136,23 +212,31 @@ export default (token: string, baseURL: string): API => {
136
212
  return subscriptions;
137
213
  },
138
214
 
139
- listSubscriptionsDeploys: async (): Promise<SubscriptionDeploy[]> =>
215
+ listSubscriptionsDeploys: (): Promise<SubscriptionDeploy[]> =>
140
216
  axios
141
217
  .get<SubscriptionDeploy[]>(
142
- baseURL + '/api/billing/list-subscriptions-deploys',
143
- {
144
- headers: { Authorization: 'jwt ' + token }
145
- }
218
+ getURL('/api/billing/list-subscriptions-deploys'),
219
+ getConfig()
146
220
  )
147
221
  .then(res => res.data),
148
222
 
149
- inspect: async (): Promise<Deployment[]> =>
223
+ inspect: (): Promise<Deployment[]> =>
150
224
  axios
151
- .get<Deployment[]>(baseURL + '/api/inspect', {
152
- headers: { Authorization: 'jwt ' + token }
153
- })
225
+ .get<Deployment[]>(getURL('/api/inspect'), getConfig())
154
226
  .then(res => res.data),
155
227
 
228
+ inspectByName: async (suffix: string): Promise<Deployment> => {
229
+ const deployments = await api.inspect();
230
+
231
+ const deploy = deployments.find(deploy => deploy.suffix == suffix);
232
+
233
+ if (!deploy) {
234
+ throw new Error(`Deployment with suffix '${suffix}' not found`);
235
+ }
236
+
237
+ return deploy;
238
+ },
239
+
156
240
  upload: async (
157
241
  name: string,
158
242
  blob: unknown,
@@ -169,14 +253,9 @@ export default (token: string, baseURL: string): API => {
169
253
  contentType: 'application/x-zip-compressed'
170
254
  });
171
255
  const res = await axios.post<string>(
172
- baseURL + '/api/package/create',
256
+ getURL('/api/package/create'),
173
257
  fd,
174
- {
175
- headers: {
176
- Authorization: 'jwt ' + token,
177
- ...(fd.getHeaders?.() ?? {}) // operator chaining to make it compatible with frontend
178
- }
179
- }
258
+ getConfig() // Axios automatically sets multipart headers
180
259
  );
181
260
  return res.data;
182
261
  },
@@ -186,30 +265,26 @@ export default (token: string, baseURL: string): API => {
186
265
  jsons: MetaCallJSON[] = []
187
266
  ): Promise<AddResponse> =>
188
267
  axios
189
- .post<string>(
190
- baseURL + '/api/repository/add',
268
+ .post<AddResponse>(
269
+ getURL('/api/repository/add'),
191
270
  {
192
271
  url,
193
272
  branch,
194
273
  jsons
195
274
  },
196
- {
197
- headers: { Authorization: 'jwt ' + token }
198
- }
275
+ getConfig()
199
276
  )
200
- .then((res: AxiosResponse) => res.data as AddResponse),
277
+ .then(res => res.data),
201
278
  branchList: (url: string): Promise<Branches> =>
202
279
  axios
203
- .post<string>(
204
- baseURL + '/api/repository/branchlist',
280
+ .post<Branches>(
281
+ getURL('/api/repository/branchlist'),
205
282
  {
206
283
  url
207
284
  },
208
- {
209
- headers: { Authorization: 'jwt ' + token }
210
- }
285
+ getConfig()
211
286
  )
212
- .then((res: AxiosResponse) => res.data as Branches),
287
+ .then(res => res.data),
213
288
 
214
289
  deploy: (
215
290
  name: string,
@@ -221,7 +296,7 @@ export default (token: string, baseURL: string): API => {
221
296
  ): Promise<Create> =>
222
297
  axios
223
298
  .post<Create>(
224
- baseURL + '/api/deploy/create',
299
+ getURL('/api/deploy/create'),
225
300
  {
226
301
  resourceType,
227
302
  suffix: name,
@@ -230,9 +305,7 @@ export default (token: string, baseURL: string): API => {
230
305
  plan,
231
306
  version
232
307
  },
233
- {
234
- headers: { Authorization: 'jwt ' + token }
235
- }
308
+ getConfig()
236
309
  )
237
310
  .then(res => res.data),
238
311
 
@@ -243,15 +316,13 @@ export default (token: string, baseURL: string): API => {
243
316
  ): Promise<string> =>
244
317
  axios
245
318
  .post<string>(
246
- baseURL + '/api/deploy/delete',
319
+ getURL('/api/deploy/delete'),
247
320
  {
248
321
  prefix,
249
322
  suffix,
250
323
  version
251
324
  },
252
- {
253
- headers: { Authorization: 'jwt ' + token }
254
- }
325
+ getConfig()
255
326
  )
256
327
  .then(res => res.data),
257
328
 
@@ -264,7 +335,7 @@ export default (token: string, baseURL: string): API => {
264
335
  ): Promise<string> =>
265
336
  axios
266
337
  .post<string>(
267
- baseURL + '/api/deploy/logs',
338
+ getURL('/api/deploy/logs'),
268
339
  {
269
340
  container,
270
341
  type,
@@ -272,26 +343,137 @@ export default (token: string, baseURL: string): API => {
272
343
  prefix,
273
344
  version
274
345
  },
275
- {
276
- headers: { Authorization: 'jwt ' + token }
277
- }
346
+ getConfig()
278
347
  )
279
348
  .then(res => res.data),
280
349
 
281
350
  fileList: (url: string, branch: string): Promise<string[]> =>
282
351
  axios
283
352
  .post<{ [k: string]: string[] }>(
284
- baseURL + '/api/repository/filelist',
353
+ getURL('/api/repository/filelist'),
285
354
  {
286
355
  url,
287
356
  branch
288
357
  },
289
- {
290
- headers: { Authorization: 'jwt ' + token }
291
- }
358
+ getConfig()
292
359
  )
293
- .then(res => res.data['files'])
360
+ .then(res => res.data['files']),
361
+
362
+ invoke: <Result, Args = unknown>(
363
+ type: InvokeType,
364
+ prefix: string,
365
+ suffix: string,
366
+ version = 'v1',
367
+ name: string,
368
+ args?: Args
369
+ ): Promise<Result> => {
370
+ const url = getURL(
371
+ `/${prefix}/${suffix}/${version}/${type}/${name}`
372
+ );
373
+ const config = getConfig();
374
+
375
+ const req =
376
+ args === undefined
377
+ ? axios.get<Result>(url, config)
378
+ : axios.post<Result>(url, args, config);
379
+
380
+ return req.then(res => res.data);
381
+ },
382
+
383
+ call: <Result, Args = unknown>(
384
+ prefix: string,
385
+ suffix: string,
386
+ version = 'v1',
387
+ name: string,
388
+ args?: Args
389
+ ): Promise<Result> =>
390
+ api.invoke(InvokeType.Call, prefix, suffix, version, name, args),
391
+
392
+ await: <Result, Args = unknown>(
393
+ prefix: string,
394
+ suffix: string,
395
+ version = 'v1',
396
+ name: string,
397
+ args?: Args
398
+ ): Promise<Result> =>
399
+ api.invoke(InvokeType.Await, prefix, suffix, version, name, args)
294
400
  };
295
401
 
296
402
  return api;
297
403
  };
404
+
405
+ export const MaxRetries = 30;
406
+ export const MaxRetryInterval = 2000;
407
+ export const MaxFuncLength = 64;
408
+
409
+ /**
410
+ * Executes an asynchronous function with automatic retry logic.
411
+ *
412
+ * The function will be retried up to `maxRetries` times, waiting `interval`
413
+ * milliseconds between each attempt. If all retries fail, the last error is
414
+ * wrapped in a new `Error` with a descriptive message, including:
415
+ * - Function name (or string representation truncated to `MaxFuncLength` chars if anonymous)
416
+ * - Number of retries attempted
417
+ * - Original error message
418
+ *
419
+ * Error handling is fully type-safe:
420
+ * - If the error is an ProtocolError (checked via `isProtocolError`), its
421
+ * message is used.
422
+ * - If the error is a standard `Error`, its `message` is used.
423
+ * - Otherwise, the error is converted to a string.
424
+ *
425
+ * @typeParam T - The return type of the function being retried.
426
+ * @param fn - A lambda or bound function returning a `Promise<T>`. The
427
+ * function should contain the logic you want to retry.
428
+ * @param maxRetries - Maximum number of retry attempts. Default: `MaxRetries`.
429
+ * @param interval - Delay between retries in milliseconds. Default: `MaxRetryInterval`.
430
+ * @returns A `Promise` resolving to the return value of `fn` if successful.
431
+ * @throws Error If all retry attempts fail, throws a new Error containing
432
+ * information about the function and the last error.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * const deployment = await waitFor(() => api.inspectByName('my-suffix'));
437
+ * ```
438
+ *
439
+ * @example
440
+ * ```ts
441
+ * const result = await waitFor(
442
+ * () => api.deploy(name, env, plan, resourceType)
443
+ * );
444
+ * ```
445
+ */
446
+ export const waitFor = async <T>(
447
+ fn: () => Promise<T>,
448
+ maxRetries: number = MaxRetries,
449
+ interval: number = MaxRetryInterval
450
+ ): Promise<T> => {
451
+ let retry = 0;
452
+
453
+ for (;;) {
454
+ try {
455
+ return await fn();
456
+ } catch (error) {
457
+ retry++;
458
+ if (retry >= maxRetries) {
459
+ const fnStr = fn.toString();
460
+ const func =
461
+ fn.name ||
462
+ (fnStr.length > MaxFuncLength
463
+ ? fnStr.slice(0, MaxFuncLength) + '...'
464
+ : fnStr);
465
+ const message = isProtocolError(error)
466
+ ? (error as ProtocolError).message
467
+ : error instanceof Error
468
+ ? error.message
469
+ : String(error);
470
+
471
+ throw new Error(
472
+ `Failed to execute '${func}' after ${maxRetries} retries: ${message}`
473
+ );
474
+ }
475
+
476
+ await new Promise(r => setTimeout(r, interval));
477
+ }
478
+ }
479
+ };
package/tsconfig.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
- "lib": ["ES2020"],
5
- "target": "ES2019",
4
+ "lib": ["es2020", "dom"],
5
+ "moduleResolution": "node",
6
+ "target": "es2020",
6
7
  "module": "CommonJS",
7
8
  "outDir": "dist",
8
9
  "esModuleInterop": true,