@metacall/protocol 0.1.28 → 0.1.30

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
@@ -6,5 +6,5 @@ export * from './plan';
6
6
  export * from './protocol';
7
7
  export * from './signup';
8
8
  export * from './token';
9
- import metacallAPI from './protocol';
10
- export default metacallAPI;
9
+ import Protocol from './protocol';
10
+ export default Protocol;
package/dist/login.js CHANGED
@@ -1,24 +1,26 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const axios_1 = __importDefault(require("axios"));
7
3
  const url_1 = require("url");
8
- exports.default = (email, password, baseURL) => {
4
+ exports.default = async (email, password, baseURL) => {
9
5
  const request = {
10
6
  email,
11
7
  password
12
8
  };
13
- if (!baseURL.includes('localhost'))
14
- request['g-recaptcha-response'] = 'empty'; //TODO: Review the captcha
15
- return axios_1.default
16
- .post(baseURL + '/login', request, {
9
+ if (!baseURL.includes('localhost')) {
10
+ request['g-recaptcha-response'] = 'empty'; // TODO: Review the captcha
11
+ }
12
+ const res = await fetch(baseURL + '/login', {
13
+ method: 'POST',
17
14
  headers: {
18
15
  Accept: 'application/json, text/plain, */*',
19
16
  Host: new url_1.URL(baseURL).host,
20
- Origin: baseURL
21
- }
22
- })
23
- .then(res => res.data);
17
+ Origin: baseURL,
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify(request)
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(res.statusText);
24
+ }
25
+ return res.text();
24
26
  };
@@ -1,13 +1,16 @@
1
- import { AxiosError } from 'axios';
2
1
  import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
3
2
  import { Plans } from './plan';
3
+ export declare class ProtocolError extends Error {
4
+ status?: number;
5
+ data?: unknown;
6
+ constructor(message: string, status?: number, data?: unknown);
7
+ }
4
8
  /**
5
- * Type guard for protocol-specific errors (Axios errors in this case).
9
+ * Type guard for protocol-specific errors.
6
10
  * @param err - The unknown error to check.
7
11
  * @returns True if the error is an ProtocolError, false otherwise.
8
12
  */
9
- export declare const isProtocolError: (err: unknown) => boolean;
10
- export { AxiosError as ProtocolError };
13
+ export declare const isProtocolError: (err: unknown) => err is ProtocolError;
11
14
  declare type SubscriptionMap = Record<string, number>;
12
15
  export interface SubscriptionDeploy {
13
16
  id: string;
@@ -19,7 +22,7 @@ export declare enum ResourceType {
19
22
  Package = "Package",
20
23
  Repository = "Repository"
21
24
  }
22
- export interface AddResponse {
25
+ export interface Resource {
23
26
  id: string;
24
27
  }
25
28
  export interface Branches {
@@ -66,8 +69,8 @@ export interface API {
66
69
  listSubscriptionsDeploys(): Promise<SubscriptionDeploy[]>;
67
70
  inspect(): Promise<Deployment[]>;
68
71
  inspectByName(suffix: string): Promise<Deployment>;
69
- upload(name: string, blob: unknown, jsons?: MetaCallJSON[], runners?: string[]): Promise<string>;
70
- add(url: string, branch: string, jsons: MetaCallJSON[]): Promise<AddResponse>;
72
+ upload(name: string, blob: unknown, jsons?: MetaCallJSON[], runners?: string[]): Promise<Resource>;
73
+ add(url: string, branch: string, jsons: MetaCallJSON[]): Promise<Resource>;
71
74
  deploy(name: string, env: {
72
75
  name: string;
73
76
  value: string;
@@ -82,8 +85,8 @@ export interface API {
82
85
  }
83
86
  declare const _default: (token: string, baseURL: string) => API;
84
87
  export default _default;
85
- export declare const MaxRetries = 30;
86
- export declare const MaxRetryInterval = 2000;
88
+ export declare const MaxRetries = 100;
89
+ export declare const MaxRetryInterval = 5000;
87
90
  export declare const MaxFuncLength = 64;
88
91
  /**
89
92
  * Executes an asynchronous function with automatic retry logic.
@@ -122,4 +125,4 @@ export declare const MaxFuncLength = 64;
122
125
  * );
123
126
  * ```
124
127
  */
125
- export declare const waitFor: <T>(fn: () => Promise<T>, maxRetries?: number, interval?: number) => Promise<T>;
128
+ export declare const waitFor: <T>(fn: (cancel: (message: string) => void) => Promise<T>, maxRetries?: number, interval?: number) => Promise<T>;
package/dist/protocol.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
2
  /*
3
-
4
- * About File:
5
-
6
- this is just a client that implements all the rest API from the FaaS, so each function it contains is an endpoint in the FaaS for deploying and similar
3
+ This is just a client that implements all the rest API from the FaaS,
4
+ so each function it contains is an endpoint in the FaaS for deploying:
7
5
 
8
6
  refresh: updates the auth token
9
7
  validate: validates the auth token
@@ -18,41 +16,26 @@
18
16
  branchList: get the branches of a repository
19
17
  fileList: get files of a repository by branch
20
18
  */
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
- };
40
- var __importDefault = (this && this.__importDefault) || function (mod) {
41
- return (mod && mod.__esModule) ? mod : { "default": mod };
42
- };
43
19
  Object.defineProperty(exports, "__esModule", { value: true });
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; } });
47
- const form_data_1 = __importDefault(require("form-data"));
20
+ exports.waitFor = exports.MaxFuncLength = exports.MaxRetryInterval = exports.MaxRetries = exports.InvokeType = exports.ResourceType = exports.isProtocolError = exports.ProtocolError = void 0;
21
+ const stream_1 = require("stream");
48
22
  const url_1 = require("url");
49
23
  const deployment_1 = require("./deployment");
24
+ class ProtocolError extends Error {
25
+ constructor(message, status, data) {
26
+ super(message);
27
+ this.name = 'ProtocolError';
28
+ this.status = status;
29
+ this.data = data;
30
+ }
31
+ }
32
+ exports.ProtocolError = ProtocolError;
50
33
  /**
51
- * Type guard for protocol-specific errors (Axios errors in this case).
34
+ * Type guard for protocol-specific errors.
52
35
  * @param err - The unknown error to check.
53
36
  * @returns True if the error is an ProtocolError, false otherwise.
54
37
  */
55
- const isProtocolError = (err) => axios_1.default.isAxiosError(err);
38
+ const isProtocolError = (err) => err instanceof ProtocolError;
56
39
  exports.isProtocolError = isProtocolError;
57
40
  var ResourceType;
58
41
  (function (ResourceType) {
@@ -64,33 +47,91 @@ var InvokeType;
64
47
  InvokeType["Call"] = "call";
65
48
  InvokeType["Await"] = "await";
66
49
  })(InvokeType = exports.InvokeType || (exports.InvokeType = {}));
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
- }
50
+ class Request {
51
+ constructor(token, baseURL) {
52
+ this.token = token;
53
+ this.baseURL = baseURL;
54
+ this.impl = {
55
+ url: '',
56
+ headers: new Headers({
57
+ Authorization: 'jwt ' + this.token
58
+ }),
59
+ method: 'GET',
60
+ body: undefined
75
61
  };
76
- };
62
+ }
63
+ url(path) {
64
+ this.impl.url = new url_1.URL(path, this.baseURL).toString();
65
+ return this;
66
+ }
67
+ headers(headers = {}) {
68
+ this.impl.headers = new Headers({
69
+ Authorization: 'jwt ' + this.token,
70
+ ...headers
71
+ });
72
+ return this;
73
+ }
74
+ method(method) {
75
+ this.impl.method = method;
76
+ return this;
77
+ }
78
+ bodyRaw(body) {
79
+ this.impl.body = body;
80
+ return this;
81
+ }
82
+ body(body) {
83
+ this.impl.body = JSON.stringify(body);
84
+ this.impl.headers.set('Content-Type', 'application/json');
85
+ return this;
86
+ }
87
+ async execute() {
88
+ const config = {
89
+ method: this.impl.method,
90
+ headers: this.impl.headers
91
+ };
92
+ if (this.impl.body !== undefined) {
93
+ config.body = this.impl.body;
94
+ }
95
+ const res = await fetch(this.impl.url, config);
96
+ if (!res.ok) {
97
+ const data = await res.text().catch(() => null);
98
+ throw new Error(`HTTP ${res.status}: ${res.statusText}${data ? ` - ${data}` : ''}`);
99
+ }
100
+ return res;
101
+ }
102
+ async asJson() {
103
+ const res = await this.execute();
104
+ return res.json();
105
+ }
106
+ async asText() {
107
+ const res = await this.execute();
108
+ return res.text();
109
+ }
110
+ async asStatus() {
111
+ const res = await this.execute();
112
+ return res.status;
113
+ }
114
+ async asResponse() {
115
+ return await this.execute();
116
+ }
117
+ }
118
+ exports.default = (token, baseURL) => {
119
+ const request = (url = baseURL) => new Request(token, url);
120
+ const hostname = new url_1.URL(baseURL).hostname;
77
121
  const api = {
78
- refresh: () => axios_1.default
79
- .get(getURL('/api/account/refresh-token'), getConfig())
80
- .then(res => res.data),
81
- ready: () => axios_1.default
82
- .get(getURL('/api/readiness'), getConfig())
83
- .then(res => res.status == 200),
84
- validate: () => axios_1.default
85
- .get(getURL('/validate'), getConfig())
86
- .then(res => res.data),
87
- deployEnabled: () => axios_1.default
88
- .get(getURL('/api/account/deploy-enabled'), getConfig())
89
- .then(res => res.data),
122
+ refresh: () => request().url('/api/account/refresh-token').asText(),
123
+ ready: () => request()
124
+ .url('/api/readiness')
125
+ .asStatus()
126
+ .then(status => status === 200),
127
+ validate: () => request().url('/validate').asJson(),
128
+ deployEnabled: () => request().url('/api/account/deploy-enabled').asJson(),
90
129
  listSubscriptions: async () => {
91
- const res = await axios_1.default.get(getURL('/api/billing/list-subscriptions'), getConfig());
130
+ const subscriptionsList = await request()
131
+ .url('/api/billing/list-subscriptions')
132
+ .asJson();
92
133
  const subscriptions = {};
93
- for (const id of res.data) {
134
+ for (const id of subscriptionsList) {
94
135
  if (subscriptions[id] === undefined) {
95
136
  subscriptions[id] = 1;
96
137
  }
@@ -100,12 +141,10 @@ exports.default = (token, baseURL) => {
100
141
  }
101
142
  return subscriptions;
102
143
  },
103
- listSubscriptionsDeploys: () => axios_1.default
104
- .get(getURL('/api/billing/list-subscriptions-deploys'), getConfig())
105
- .then(res => res.data),
106
- inspect: () => axios_1.default
107
- .get(getURL('/api/inspect'), getConfig())
108
- .then(res => res.data),
144
+ listSubscriptionsDeploys: () => request()
145
+ .url('/api/billing/list-subscriptions')
146
+ .asJson(),
147
+ inspect: () => request().url('/api/inspect').asJson(),
109
148
  inspectByName: async (suffix) => {
110
149
  const deployments = await api.inspect();
111
150
  const deploy = deployments.find(deploy => deploy.suffix == suffix);
@@ -114,79 +153,118 @@ exports.default = (token, baseURL) => {
114
153
  }
115
154
  return deploy;
116
155
  },
117
- upload: async (name, blob, jsons = [], runners = []) => {
118
- const fd = new form_data_1.default();
156
+ upload: async (name, data, jsons = [], runners = []) => {
157
+ const fd = new FormData();
119
158
  fd.append('id', name);
120
159
  fd.append('type', 'application/x-zip-compressed');
121
160
  fd.append('jsons', JSON.stringify(jsons));
122
161
  fd.append('runners', JSON.stringify(runners));
123
- fd.append('raw', blob, {
124
- filename: 'blob',
125
- contentType: 'application/x-zip-compressed'
126
- });
127
- const res = await axios_1.default.post(getURL('/api/package/create'), fd, getConfig() // Axios automatically sets multipart headers
128
- );
129
- return res.data;
162
+ if (data instanceof Blob) {
163
+ fd.append('raw', data, `${name}.zip`);
164
+ }
165
+ else if (data instanceof stream_1.Readable) {
166
+ // This is terrible but NodeJS does not ensure that streaming and zero
167
+ // copy will be performed anyway, as the sizes are not really big (150mb is the limit)
168
+ // we can do this nasty intermediate buffer creation and forget about it
169
+ const chunks = [];
170
+ for await (const chunk of data) {
171
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
172
+ }
173
+ const buffer = Buffer.concat(chunks);
174
+ fd.append('raw', new Blob([buffer]), `${name}.zip`);
175
+ }
176
+ else {
177
+ throw Error(`Type ${typeof data} not supported, use Blob or Readable`);
178
+ }
179
+ return await request()
180
+ .url('/api/package/create')
181
+ .method('POST')
182
+ .bodyRaw(fd)
183
+ .asJson();
130
184
  },
131
- add: (url, branch, jsons = []) => axios_1.default
132
- .post(getURL('/api/repository/add'), {
185
+ add: (url, branch, jsons = []) => request()
186
+ .url('/api/repository/add')
187
+ .method('POST')
188
+ .body({
133
189
  url,
134
190
  branch,
135
191
  jsons
136
- }, getConfig())
137
- .then(res => res.data),
138
- branchList: (url) => axios_1.default
139
- .post(getURL('/api/repository/branchlist'), {
192
+ })
193
+ .asJson(),
194
+ branchList: (url) => request()
195
+ .url('/api/repository/branchlist')
196
+ .method('POST')
197
+ .body({
140
198
  url
141
- }, getConfig())
142
- .then(res => res.data),
143
- deploy: (name, env, plan, resourceType, release = Date.now().toString(16), version = 'v1') => axios_1.default
144
- .post(getURL('/api/deploy/create'), {
199
+ })
200
+ .asJson(),
201
+ deploy: (name, env, plan, resourceType, release = Date.now().toString(16), version = 'v1') => request()
202
+ .url('/api/deploy/create')
203
+ .method('POST')
204
+ .body({
145
205
  resourceType,
146
206
  suffix: name,
147
207
  release,
148
208
  env,
149
209
  plan,
150
210
  version
151
- }, getConfig())
152
- .then(res => res.data),
153
- deployDelete: (prefix, suffix, version = 'v1') => axios_1.default
154
- .post(getURL('/api/deploy/delete'), {
211
+ })
212
+ .asJson(),
213
+ deployDelete: (prefix, suffix, version = 'v1') => request()
214
+ .url('/api/deploy/delete')
215
+ .method('POST')
216
+ .body({
155
217
  prefix,
156
218
  suffix,
157
219
  version
158
- }, getConfig())
159
- .then(res => res.data),
160
- logs: (container, type = deployment_1.LogType.Deploy, suffix, prefix, version = 'v1') => axios_1.default
161
- .post(getURL('/api/deploy/logs'), {
220
+ })
221
+ .asJson(),
222
+ logs: (container, type = deployment_1.LogType.Deploy, suffix, prefix, version = 'v1') => request()
223
+ .url('/api/deploy/logs')
224
+ .method('POST')
225
+ .body({
162
226
  container,
163
227
  type,
164
228
  suffix,
165
229
  prefix,
166
230
  version
167
- }, getConfig())
168
- .then(res => res.data),
169
- fileList: (url, branch) => axios_1.default
170
- .post(getURL('/api/repository/filelist'), {
231
+ })
232
+ .asJson(),
233
+ fileList: (url, branch) => request()
234
+ .url('/api/repository/filelist')
235
+ .method('POST')
236
+ .body({
171
237
  url,
172
238
  branch
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);
239
+ })
240
+ .asJson()
241
+ .then(res => res['files']),
242
+ invoke: async (type, prefix, suffix, version = 'v1', name, args) => {
243
+ const req = (() => {
244
+ if (hostname === 'localhost') {
245
+ // Old API in commercial FaaS and current API of FaaS reimplementation
246
+ return request().url(`/${prefix}/${suffix}/${version}/${type}/${name}`);
247
+ }
248
+ else {
249
+ // New API used by commercial FaaS
250
+ return request(`https://${version}-${suffix}-${prefix}.api.metacall.io`).url(`/${type}/${name}`);
251
+ }
252
+ })();
253
+ if (args === undefined) {
254
+ req.method('GET');
255
+ }
256
+ else {
257
+ req.method('POST').body(args);
258
+ }
259
+ return await req.asJson();
182
260
  },
183
261
  call: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Call, prefix, suffix, version, name, args),
184
262
  await: (prefix, suffix, version = 'v1', name, args) => api.invoke(InvokeType.Await, prefix, suffix, version, name, args)
185
263
  };
186
264
  return api;
187
265
  };
188
- exports.MaxRetries = 30;
189
- exports.MaxRetryInterval = 2000;
266
+ exports.MaxRetries = 100;
267
+ exports.MaxRetryInterval = 5000;
190
268
  exports.MaxFuncLength = 64;
191
269
  /**
192
270
  * Executes an asynchronous function with automatic retry logic.
@@ -227,9 +305,14 @@ exports.MaxFuncLength = 64;
227
305
  */
228
306
  const waitFor = async (fn, maxRetries = exports.MaxRetries, interval = exports.MaxRetryInterval) => {
229
307
  let retry = 0;
308
+ let cancellation = undefined;
309
+ const cancel = (message) => {
310
+ retry = exports.MaxRetries;
311
+ cancellation = `Operation cancelled with message: ${message}`;
312
+ };
230
313
  for (;;) {
231
314
  try {
232
- return await fn();
315
+ return await fn(cancel);
233
316
  }
234
317
  catch (error) {
235
318
  retry++;
@@ -239,11 +322,13 @@ const waitFor = async (fn, maxRetries = exports.MaxRetries, interval = exports.M
239
322
  (fnStr.length > exports.MaxFuncLength
240
323
  ? fnStr.slice(0, exports.MaxFuncLength) + '...'
241
324
  : fnStr);
242
- const message = exports.isProtocolError(error)
243
- ? error.message
244
- : error instanceof Error
325
+ const message = cancellation !== undefined
326
+ ? cancellation
327
+ : exports.isProtocolError(error)
245
328
  ? error.message
246
- : String(error);
329
+ : error instanceof Error
330
+ ? error.message
331
+ : String(error);
247
332
  throw new Error(`Failed to execute '${func}' after ${maxRetries} retries: ${message}`);
248
333
  }
249
334
  await new Promise(r => setTimeout(r, interval));
package/dist/signup.js CHANGED
@@ -1,25 +1,27 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const axios_1 = __importDefault(require("axios"));
7
3
  const url_1 = require("url");
8
- exports.default = (email, password, alias, baseURL) => {
4
+ exports.default = async (email, password, alias, baseURL) => {
9
5
  const request = {
10
6
  email,
11
7
  password,
12
8
  alias
13
9
  };
14
- if (!baseURL.includes('localhost'))
15
- request['g-recaptcha-response'] = 'empty';
16
- return axios_1.default
17
- .post(baseURL + '/signup', request, {
10
+ if (!baseURL.includes('localhost')) {
11
+ request['g-recaptcha-response'] = 'empty'; // TODO: Review the captcha
12
+ }
13
+ const res = await fetch(baseURL + '/signup', {
14
+ method: 'POST',
18
15
  headers: {
19
16
  Accept: 'application/json, text/plain, */*',
20
17
  Host: new url_1.URL(baseURL).host,
21
- Origin: baseURL
22
- }
23
- })
24
- .then(res => res.data);
18
+ Origin: baseURL,
19
+ 'Content-Type': 'application/json'
20
+ },
21
+ body: JSON.stringify(request)
22
+ });
23
+ if (!res.ok) {
24
+ throw new Error(res.statusText);
25
+ }
26
+ return res.text();
25
27
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metacall/protocol",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Tool for deploying into MetaCall FaaS platform.",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js",
@@ -18,8 +18,9 @@
18
18
  }
19
19
  },
20
20
  "scripts": {
21
+ "pretest": "npm --prefix ./src/test/resources/integration/api install",
21
22
  "test": "npm run --silent build && mocha dist/test",
22
- "unit": "npm run --silent test -- --ignore **/integration**",
23
+ "unit": "npm run --silent test -- --ignore **/**.integration.**",
23
24
  "prepublishOnly": "npm run --silent build",
24
25
  "postinstall": "node -e \"require('fs').existsSync('githooks') && require('./githooks/configure.js').configure()\"",
25
26
  "build": "npm run --silent lint && tsc",
@@ -85,8 +86,6 @@
85
86
  }
86
87
  },
87
88
  "dependencies": {
88
- "axios": "^1.13.5",
89
- "form-data": "^3.0.0",
90
89
  "ignore-walk": "^3.0.4",
91
90
  "jsonwebtoken": "^9.0.0"
92
91
  },
package/src/index.ts CHANGED
@@ -7,5 +7,5 @@ export * from './protocol';
7
7
  export * from './signup';
8
8
  export * from './token';
9
9
 
10
- import metacallAPI from './protocol';
11
- export default metacallAPI;
10
+ import Protocol from './protocol';
11
+ export default Protocol;
package/src/login.ts CHANGED
@@ -1,4 +1,3 @@
1
- import axios from 'axios';
2
1
  import { URL } from 'url';
3
2
 
4
3
  interface Request {
@@ -7,7 +6,7 @@ interface Request {
7
6
  'g-recaptcha-response'?: string;
8
7
  }
9
8
 
10
- export default (
9
+ export default async (
11
10
  email: string,
12
11
  password: string,
13
12
  baseURL: string
@@ -17,16 +16,24 @@ export default (
17
16
  password
18
17
  };
19
18
 
20
- if (!baseURL.includes('localhost'))
21
- request['g-recaptcha-response'] = 'empty'; //TODO: Review the captcha
19
+ if (!baseURL.includes('localhost')) {
20
+ request['g-recaptcha-response'] = 'empty'; // TODO: Review the captcha
21
+ }
22
22
 
23
- return axios
24
- .post<string>(baseURL + '/login', request, {
25
- headers: {
26
- Accept: 'application/json, text/plain, */*',
27
- Host: new URL(baseURL).host,
28
- Origin: baseURL
29
- }
30
- })
31
- .then(res => res.data);
23
+ const res = await fetch(baseURL + '/login', {
24
+ method: 'POST',
25
+ headers: {
26
+ Accept: 'application/json, text/plain, */*',
27
+ Host: new URL(baseURL).host,
28
+ Origin: baseURL,
29
+ 'Content-Type': 'application/json'
30
+ },
31
+ body: JSON.stringify(request)
32
+ });
33
+
34
+ if (!res.ok) {
35
+ throw new Error(res.statusText);
36
+ }
37
+
38
+ return res.text();
32
39
  };
package/src/protocol.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  /*
2
-
3
- * About File:
4
-
5
- this is just a client that implements all the rest API from the FaaS, so each function it contains is an endpoint in the FaaS for deploying and similar
2
+ This is just a client that implements all the rest API from the FaaS,
3
+ so each function it contains is an endpoint in the FaaS for deploying:
6
4
 
7
5
  refresh: updates the auth token
8
6
  validate: validates the auth token
@@ -18,22 +16,30 @@
18
16
  fileList: get files of a repository by branch
19
17
  */
20
18
 
21
- import axios, { AxiosError, AxiosRequestConfig } from 'axios';
22
- import FormData from 'form-data';
19
+ import { Readable } from 'stream';
23
20
  import { URL } from 'url';
24
21
  import { Create, Deployment, LogType, MetaCallJSON } from './deployment';
25
22
  import { Plans } from './plan';
26
- import { ProtocolError } from './protocol';
23
+
24
+ export class ProtocolError extends Error {
25
+ status?: number;
26
+ data?: unknown;
27
+
28
+ constructor(message: string, status?: number, data?: unknown) {
29
+ super(message);
30
+ this.name = 'ProtocolError';
31
+ this.status = status;
32
+ this.data = data;
33
+ }
34
+ }
27
35
 
28
36
  /**
29
- * Type guard for protocol-specific errors (Axios errors in this case).
37
+ * Type guard for protocol-specific errors.
30
38
  * @param err - The unknown error to check.
31
39
  * @returns True if the error is an ProtocolError, false otherwise.
32
40
  */
33
- export const isProtocolError = (err: unknown): boolean =>
34
- axios.isAxiosError(err);
35
-
36
- export { AxiosError as ProtocolError };
41
+ export const isProtocolError = (err: unknown): err is ProtocolError =>
42
+ err instanceof ProtocolError;
37
43
 
38
44
  type SubscriptionMap = Record<string, number>;
39
45
 
@@ -49,7 +55,7 @@ export enum ResourceType {
49
55
  Repository = 'Repository'
50
56
  }
51
57
 
52
- export interface AddResponse {
58
+ export interface Resource {
53
59
  id: string;
54
60
  }
55
61
 
@@ -106,12 +112,8 @@ export interface API {
106
112
  blob: unknown,
107
113
  jsons?: MetaCallJSON[],
108
114
  runners?: string[]
109
- ): Promise<string>;
110
- add(
111
- url: string,
112
- branch: string,
113
- jsons: MetaCallJSON[]
114
- ): Promise<AddResponse>;
115
+ ): Promise<Resource>;
116
+ add(url: string, branch: string, jsons: MetaCallJSON[]): Promise<Resource>;
115
117
  deploy(
116
118
  name: string,
117
119
  env: { name: string; value: string }[],
@@ -158,50 +160,132 @@ export interface API {
158
160
  ): Promise<Result>;
159
161
  }
160
162
 
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
- }
163
+ interface RequestImpl {
164
+ url: string;
165
+ headers: Headers;
166
+ method: string;
167
+ body?: BodyInit;
168
+ }
169
+
170
+ class Request {
171
+ private token: string;
172
+ private baseURL: string;
173
+ private impl: RequestImpl;
174
+
175
+ constructor(token: string, baseURL: string) {
176
+ this.token = token;
177
+ this.baseURL = baseURL;
178
+ this.impl = {
179
+ url: '',
180
+ headers: new Headers({
181
+ Authorization: 'jwt ' + this.token
182
+ }),
183
+ method: 'GET',
184
+ body: undefined
169
185
  };
170
- };
186
+ }
187
+
188
+ url(path: string): Request {
189
+ this.impl.url = new URL(path, this.baseURL).toString();
190
+ return this;
191
+ }
192
+
193
+ headers(headers = {}): Request {
194
+ this.impl.headers = new Headers({
195
+ Authorization: 'jwt ' + this.token,
196
+ ...headers
197
+ });
198
+ return this;
199
+ }
200
+
201
+ method(method: string): Request {
202
+ this.impl.method = method;
203
+ return this;
204
+ }
205
+
206
+ bodyRaw(body: BodyInit): Request {
207
+ this.impl.body = body;
208
+ return this;
209
+ }
210
+
211
+ body(body: unknown): Request {
212
+ this.impl.body = JSON.stringify(body);
213
+ this.impl.headers.set('Content-Type', 'application/json');
214
+ return this;
215
+ }
216
+
217
+ private async execute(): Promise<Response> {
218
+ const config: RequestInit = {
219
+ method: this.impl.method,
220
+ headers: this.impl.headers
221
+ };
222
+
223
+ if (this.impl.body !== undefined) {
224
+ config.body = this.impl.body;
225
+ }
226
+
227
+ const res = await fetch(this.impl.url, config);
228
+
229
+ if (!res.ok) {
230
+ const data = await res.text().catch(() => null);
231
+ throw new Error(
232
+ `HTTP ${res.status}: ${res.statusText}${
233
+ data ? ` - ${data}` : ''
234
+ }`
235
+ );
236
+ }
237
+
238
+ return res;
239
+ }
240
+
241
+ async asJson<T>(): Promise<T> {
242
+ const res = await this.execute();
243
+ return res.json() as Promise<T>;
244
+ }
245
+
246
+ async asText(): Promise<string> {
247
+ const res = await this.execute();
248
+ return res.text();
249
+ }
250
+
251
+ async asStatus(): Promise<number> {
252
+ const res = await this.execute();
253
+ return res.status;
254
+ }
255
+
256
+ async asResponse(): Promise<Response> {
257
+ return await this.execute();
258
+ }
259
+ }
260
+
261
+ export default (token: string, baseURL: string): API => {
262
+ const request = (url = baseURL) => new Request(token, url);
263
+ const hostname = new URL(baseURL).hostname;
171
264
 
172
265
  const api: API = {
173
266
  refresh: (): Promise<string> =>
174
- axios
175
- .get<string>(getURL('/api/account/refresh-token'), getConfig())
176
- .then(res => res.data),
267
+ request().url('/api/account/refresh-token').asText(),
177
268
 
178
269
  ready: (): Promise<boolean> =>
179
- axios
180
- .get<boolean>(getURL('/api/readiness'), getConfig())
181
- .then(res => res.status == 200),
270
+ request()
271
+ .url('/api/readiness')
272
+ .asStatus()
273
+ .then(status => status === 200),
182
274
 
183
275
  validate: (): Promise<boolean> =>
184
- axios
185
- .get<boolean>(getURL('/validate'), getConfig())
186
- .then(res => res.data),
276
+ request().url('/validate').asJson<boolean>(),
187
277
 
188
278
  deployEnabled: (): Promise<boolean> =>
189
- axios
190
- .get<boolean>(
191
- getURL('/api/account/deploy-enabled'),
192
- getConfig()
193
- )
194
- .then(res => res.data),
279
+ request().url('/api/account/deploy-enabled').asJson<boolean>(),
195
280
 
196
281
  listSubscriptions: async (): Promise<SubscriptionMap> => {
197
- const res = await axios.get<string[]>(
198
- getURL('/api/billing/list-subscriptions'),
199
- getConfig()
200
- );
282
+ const subscriptionsList = await request()
283
+ .url('/api/billing/list-subscriptions')
284
+ .asJson<string[]>();
201
285
 
202
286
  const subscriptions: SubscriptionMap = {};
203
287
 
204
- for (const id of res.data) {
288
+ for (const id of subscriptionsList) {
205
289
  if (subscriptions[id] === undefined) {
206
290
  subscriptions[id] = 1;
207
291
  } else {
@@ -213,17 +297,12 @@ export default (token: string, baseURL: string): API => {
213
297
  },
214
298
 
215
299
  listSubscriptionsDeploys: (): Promise<SubscriptionDeploy[]> =>
216
- axios
217
- .get<SubscriptionDeploy[]>(
218
- getURL('/api/billing/list-subscriptions-deploys'),
219
- getConfig()
220
- )
221
- .then(res => res.data),
300
+ request()
301
+ .url('/api/billing/list-subscriptions')
302
+ .asJson<SubscriptionDeploy[]>(),
222
303
 
223
304
  inspect: (): Promise<Deployment[]> =>
224
- axios
225
- .get<Deployment[]>(getURL('/api/inspect'), getConfig())
226
- .then(res => res.data),
305
+ request().url('/api/inspect').asJson<Deployment[]>(),
227
306
 
228
307
  inspectByName: async (suffix: string): Promise<Deployment> => {
229
308
  const deployments = await api.inspect();
@@ -239,52 +318,67 @@ export default (token: string, baseURL: string): API => {
239
318
 
240
319
  upload: async (
241
320
  name: string,
242
- blob: unknown,
321
+ data: Blob | Readable,
243
322
  jsons: MetaCallJSON[] = [],
244
323
  runners: string[] = []
245
- ): Promise<string> => {
324
+ ): Promise<Resource> => {
246
325
  const fd = new FormData();
326
+
247
327
  fd.append('id', name);
248
328
  fd.append('type', 'application/x-zip-compressed');
249
329
  fd.append('jsons', JSON.stringify(jsons));
250
330
  fd.append('runners', JSON.stringify(runners));
251
- fd.append('raw', blob, {
252
- filename: 'blob',
253
- contentType: 'application/x-zip-compressed'
254
- });
255
- const res = await axios.post<string>(
256
- getURL('/api/package/create'),
257
- fd,
258
- getConfig() // Axios automatically sets multipart headers
259
- );
260
- return res.data;
331
+
332
+ if (data instanceof Blob) {
333
+ fd.append('raw', data, `${name}.zip`);
334
+ } else if (data instanceof Readable) {
335
+ // This is terrible but NodeJS does not ensure that streaming and zero
336
+ // copy will be performed anyway, as the sizes are not really big (150mb is the limit)
337
+ // we can do this nasty intermediate buffer creation and forget about it
338
+ const chunks: Uint8Array[] = [];
339
+ for await (const chunk of data) {
340
+ chunks.push(
341
+ typeof chunk === 'string' ? Buffer.from(chunk) : chunk
342
+ );
343
+ }
344
+ const buffer = Buffer.concat(chunks);
345
+ fd.append('raw', new Blob([buffer]), `${name}.zip`);
346
+ } else {
347
+ throw Error(
348
+ `Type ${typeof data} not supported, use Blob or Readable`
349
+ );
350
+ }
351
+
352
+ return await request()
353
+ .url('/api/package/create')
354
+ .method('POST')
355
+ .bodyRaw(fd)
356
+ .asJson<Resource>();
261
357
  },
358
+
262
359
  add: (
263
360
  url: string,
264
361
  branch: string,
265
362
  jsons: MetaCallJSON[] = []
266
- ): Promise<AddResponse> =>
267
- axios
268
- .post<AddResponse>(
269
- getURL('/api/repository/add'),
270
- {
271
- url,
272
- branch,
273
- jsons
274
- },
275
- getConfig()
276
- )
277
- .then(res => res.data),
363
+ ): Promise<Resource> =>
364
+ request()
365
+ .url('/api/repository/add')
366
+ .method('POST')
367
+ .body({
368
+ url,
369
+ branch,
370
+ jsons
371
+ })
372
+ .asJson<Resource>(),
373
+
278
374
  branchList: (url: string): Promise<Branches> =>
279
- axios
280
- .post<Branches>(
281
- getURL('/api/repository/branchlist'),
282
- {
283
- url
284
- },
285
- getConfig()
286
- )
287
- .then(res => res.data),
375
+ request()
376
+ .url('/api/repository/branchlist')
377
+ .method('POST')
378
+ .body({
379
+ url
380
+ })
381
+ .asJson<Branches>(),
288
382
 
289
383
  deploy: (
290
384
  name: string,
@@ -294,37 +388,33 @@ export default (token: string, baseURL: string): API => {
294
388
  release: string = Date.now().toString(16),
295
389
  version = 'v1'
296
390
  ): Promise<Create> =>
297
- axios
298
- .post<Create>(
299
- getURL('/api/deploy/create'),
300
- {
301
- resourceType,
302
- suffix: name,
303
- release,
304
- env,
305
- plan,
306
- version
307
- },
308
- getConfig()
309
- )
310
- .then(res => res.data),
391
+ request()
392
+ .url('/api/deploy/create')
393
+ .method('POST')
394
+ .body({
395
+ resourceType,
396
+ suffix: name,
397
+ release,
398
+ env,
399
+ plan,
400
+ version
401
+ })
402
+ .asJson<Create>(),
311
403
 
312
404
  deployDelete: (
313
405
  prefix: string,
314
406
  suffix: string,
315
407
  version = 'v1'
316
408
  ): Promise<string> =>
317
- axios
318
- .post<string>(
319
- getURL('/api/deploy/delete'),
320
- {
321
- prefix,
322
- suffix,
323
- version
324
- },
325
- getConfig()
326
- )
327
- .then(res => res.data),
409
+ request()
410
+ .url('/api/deploy/delete')
411
+ .method('POST')
412
+ .body({
413
+ prefix,
414
+ suffix,
415
+ version
416
+ })
417
+ .asJson<string>(),
328
418
 
329
419
  logs: (
330
420
  container: string,
@@ -333,33 +423,30 @@ export default (token: string, baseURL: string): API => {
333
423
  prefix: string,
334
424
  version = 'v1'
335
425
  ): Promise<string> =>
336
- axios
337
- .post<string>(
338
- getURL('/api/deploy/logs'),
339
- {
340
- container,
341
- type,
342
- suffix,
343
- prefix,
344
- version
345
- },
346
- getConfig()
347
- )
348
- .then(res => res.data),
426
+ request()
427
+ .url('/api/deploy/logs')
428
+ .method('POST')
429
+ .body({
430
+ container,
431
+ type,
432
+ suffix,
433
+ prefix,
434
+ version
435
+ })
436
+ .asJson<string>(),
349
437
 
350
438
  fileList: (url: string, branch: string): Promise<string[]> =>
351
- axios
352
- .post<{ [k: string]: string[] }>(
353
- getURL('/api/repository/filelist'),
354
- {
355
- url,
356
- branch
357
- },
358
- getConfig()
359
- )
360
- .then(res => res.data['files']),
361
-
362
- invoke: <Result, Args = unknown>(
439
+ request()
440
+ .url('/api/repository/filelist')
441
+ .method('POST')
442
+ .body({
443
+ url,
444
+ branch
445
+ })
446
+ .asJson<{ [k: string]: string[] }>()
447
+ .then(res => res['files']),
448
+
449
+ invoke: async <Result, Args = unknown>(
363
450
  type: InvokeType,
364
451
  prefix: string,
365
452
  suffix: string,
@@ -367,17 +454,27 @@ export default (token: string, baseURL: string): API => {
367
454
  name: string,
368
455
  args?: Args
369
456
  ): Promise<Result> => {
370
- const url = getURL(
371
- `/${prefix}/${suffix}/${version}/${type}/${name}`
372
- );
373
- const config = getConfig();
457
+ const req = (() => {
458
+ if (hostname === 'localhost') {
459
+ // Old API in commercial FaaS and current API of FaaS reimplementation
460
+ return request().url(
461
+ `/${prefix}/${suffix}/${version}/${type}/${name}`
462
+ );
463
+ } else {
464
+ // New API used by commercial FaaS
465
+ return request(
466
+ `https://${version}-${suffix}-${prefix}.api.metacall.io`
467
+ ).url(`/${type}/${name}`);
468
+ }
469
+ })();
374
470
 
375
- const req =
376
- args === undefined
377
- ? axios.get<Result>(url, config)
378
- : axios.post<Result>(url, args, config);
471
+ if (args === undefined) {
472
+ req.method('GET');
473
+ } else {
474
+ req.method('POST').body(args);
475
+ }
379
476
 
380
- return req.then(res => res.data);
477
+ return await req.asJson<Result>();
381
478
  },
382
479
 
383
480
  call: <Result, Args = unknown>(
@@ -402,8 +499,8 @@ export default (token: string, baseURL: string): API => {
402
499
  return api;
403
500
  };
404
501
 
405
- export const MaxRetries = 30;
406
- export const MaxRetryInterval = 2000;
502
+ export const MaxRetries = 100;
503
+ export const MaxRetryInterval = 5000;
407
504
  export const MaxFuncLength = 64;
408
505
 
409
506
  /**
@@ -444,15 +541,21 @@ export const MaxFuncLength = 64;
444
541
  * ```
445
542
  */
446
543
  export const waitFor = async <T>(
447
- fn: () => Promise<T>,
544
+ fn: (cancel: (message: string) => void) => Promise<T>,
448
545
  maxRetries: number = MaxRetries,
449
546
  interval: number = MaxRetryInterval
450
547
  ): Promise<T> => {
451
548
  let retry = 0;
549
+ let cancellation = undefined;
550
+
551
+ const cancel = (message: string) => {
552
+ retry = MaxRetries;
553
+ cancellation = `Operation cancelled with message: ${message}`;
554
+ };
452
555
 
453
556
  for (;;) {
454
557
  try {
455
- return await fn();
558
+ return await fn(cancel);
456
559
  } catch (error) {
457
560
  retry++;
458
561
  if (retry >= maxRetries) {
@@ -462,11 +565,14 @@ export const waitFor = async <T>(
462
565
  (fnStr.length > MaxFuncLength
463
566
  ? fnStr.slice(0, MaxFuncLength) + '...'
464
567
  : fnStr);
465
- const message = isProtocolError(error)
466
- ? (error as ProtocolError).message
467
- : error instanceof Error
468
- ? error.message
469
- : String(error);
568
+ const message =
569
+ cancellation !== undefined
570
+ ? cancellation
571
+ : isProtocolError(error)
572
+ ? error.message
573
+ : error instanceof Error
574
+ ? error.message
575
+ : String(error);
470
576
 
471
577
  throw new Error(
472
578
  `Failed to execute '${func}' after ${maxRetries} retries: ${message}`
package/src/signup.ts CHANGED
@@ -1,4 +1,3 @@
1
- import axios from 'axios';
2
1
  import { URL } from 'url';
3
2
  interface Request {
4
3
  email: string;
@@ -7,7 +6,7 @@ interface Request {
7
6
  'g-recaptcha-response'?: string;
8
7
  }
9
8
 
10
- export default (
9
+ export default async (
11
10
  email: string,
12
11
  password: string,
13
12
  alias: string,
@@ -18,15 +17,25 @@ export default (
18
17
  password,
19
18
  alias
20
19
  };
21
- if (!baseURL.includes('localhost'))
22
- request['g-recaptcha-response'] = 'empty';
23
- return axios
24
- .post<string>(baseURL + '/signup', request, {
25
- headers: {
26
- Accept: 'application/json, text/plain, */*',
27
- Host: new URL(baseURL).host,
28
- Origin: baseURL
29
- }
30
- })
31
- .then(res => res.data);
20
+
21
+ if (!baseURL.includes('localhost')) {
22
+ request['g-recaptcha-response'] = 'empty'; // TODO: Review the captcha
23
+ }
24
+
25
+ const res = await fetch(baseURL + '/signup', {
26
+ method: 'POST',
27
+ headers: {
28
+ Accept: 'application/json, text/plain, */*',
29
+ Host: new URL(baseURL).host,
30
+ Origin: baseURL,
31
+ 'Content-Type': 'application/json'
32
+ },
33
+ body: JSON.stringify(request)
34
+ });
35
+
36
+ if (!res.ok) {
37
+ throw new Error(res.statusText);
38
+ }
39
+
40
+ return res.text();
32
41
  };
package/tsconfig.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
- "lib": ["es2020", "dom"],
4
+ "lib": ["es2020", "DOM"],
5
5
  "moduleResolution": "node",
6
6
  "target": "es2020",
7
7
  "module": "CommonJS",