UNPKG

7.02 kBJavaScriptView Raw
1const https = require("https");
2const isEmpty = require("lodash.isempty");
3const debug = require("debug")("buzzapi");
4const delay = require("delay");
5const hyperid = require("hyperid")({ fixedLength: true, urlSafe: true });
6const os = require("os");
7const BuzzAPIError = require("./buzzApiError");
8const pRetry = require("p-retry");
9const pThrottle = require("p-throttle");
10const { default: PQueue } = require("p-queue");
11
12const BuzzAPI = function(config) {
13 const that = this;
14
15 const queue = new PQueue({ concurrency: 20 });
16
17 this.options = {
18 api_app_id: config.apiUser,
19 api_app_password: Buffer.from(config.apiPassword).toString("base64"),
20 api_request_mode: config.sync ? "sync" : "async",
21 api_receive_timeout: config.api_receive_timeout || 900000
22 };
23
24 const server = config.server || "https://api.gatech.edu";
25
26 const fetch = pThrottle(
27 require("make-fetch-happen").defaults({
28 agent: https.globalAgent
29 }),
30 333,
31 1000
32 );
33
34 const unresolved = {};
35
36 this.post = function(resource, operation, data) {
37 debug("Options: " + JSON.stringify(that.options));
38 return queue.add(
39 () =>
40 new Promise((res, rej) => {
41 const handle =
42 data.api_client_request_handle ||
43 `${process.pid}@${os.hostname()}-${hyperid()}`;
44
45 const myOpts = {
46 method: "POST",
47 body: JSON.stringify({
48 ...that.options,
49 ...data,
50 api_client_request_handle: handle
51 }),
52 headers: { "Content-Type": "application/json" }
53 };
54 debug("Requesting %s", JSON.stringify(myOpts));
55 return pRetry(
56 () => fetch(`${server}/apiv3/${resource}/${operation}`, myOpts),
57 { retries: 5, randomize: true }
58 ).then(response => {
59 if (!response.ok) {
60 return rej(
61 new BuzzAPIError(
62 response.statusText,
63 null,
64 response.statusText,
65 response.api_request_messageid
66 )
67 );
68 }
69 return response.json().then(json => {
70 if (json.api_error_info) {
71 const error = new BuzzAPIError(
72 new Error(),
73 json.api_error_info,
74 json,
75 json.api_request_messageid
76 );
77 return rej(error);
78 } else if (that.options.api_request_mode === "sync") {
79 debug("Sync was set, returning the result");
80 return res(json.api_result_data);
81 } else {
82 debug("Got messageId: %s for %s", json.api_result_data, handle);
83 unresolved[json.api_result_data] = {
84 resolve: res,
85 reject: rej,
86 initTime: new Date()
87 };
88 that.options.ticket = json.api_app_ticket;
89 return getResult();
90 }
91 });
92 });
93 })
94 );
95 };
96
97 const resolve = function(messageId, result) {
98 debug("size: %s, pending: %s", queue.size, queue.pending);
99 const message = unresolved[messageId];
100 delete unresolved[messageId];
101 return message.resolve(result);
102 };
103
104 const reject = function(messageId, err) {
105 debug("size: %s, pending: %s", queue.size, queue.pending);
106 const message = unresolved[messageId];
107 delete unresolved[messageId];
108 return message.reject(err);
109 };
110
111 const cleanupExpired = function() {
112 Object.keys(unresolved).forEach(messageId => {
113 if (
114 new Date() - unresolved[messageId].initTime >
115 that.options.api_receive_timeout
116 ) {
117 const err = new Error("Request timed out for: " + messageId);
118 return reject(messageId, err);
119 }
120 });
121 return;
122 };
123
124 const scheduleRetry = function() {
125 return delay(Math.floor(Math.random() * 4000 + 1000)).then(() =>
126 getResult()
127 );
128 };
129
130 const getResult = function() {
131 cleanupExpired();
132 const messageIds = Object.keys(unresolved);
133 if (messageIds.length === 0) {
134 return new Promise(res => res());
135 }
136 const handle = `${process.pid}@${os.hostname()}-${hyperid()}`;
137 debug("Asking for result of %s using handle %s", messageIds, handle);
138 return pRetry(
139 () =>
140 fetch(`${server}/apiv3/api.my_messages`, {
141 method: "POST",
142 body: JSON.stringify({
143 api_operation: "read",
144 api_app_ticket: that.options.ticket,
145 api_pull_response_to: messageIds,
146 api_receive_timeout: 5000,
147 api_client_request_handle: handle
148 }),
149 headers: { "Content-Type": "application/json" }
150 }),
151 { retries: 5, randomize: true }
152 ).then(response => {
153 if (!response.ok) {
154 return scheduleRetry();
155 }
156 return response.json().then(json => {
157 if (
158 json.api_error_info ||
159 (json.api_result_data && json.api_result_data.api_error_info)
160 ) {
161 if (json.api_error_info) {
162 const message =
163 unresolved[json.api_error_info.api_request_messageid];
164 const err = new BuzzAPIError(
165 "BuzzApi returned error_info",
166 json.api_error_info,
167 json,
168 json.api_error_info.api_request_messageid
169 );
170 debug(unresolved);
171 if (message) {
172 return reject(json.api_error_info.api_request_messageid, err);
173 } else {
174 return Promise.reject(err);
175 }
176 } else if (json.api_result_data) {
177 const messageId = json.api_result_data.api_request_messageid;
178 const err = new BuzzAPIError(
179 "BuzzApi returned error_info",
180 json.api_result_data.api_error_info,
181 json,
182 messageId
183 );
184 return reject(messageId, err);
185 } else {
186 return Promise.reject(
187 new BuzzAPIError(
188 new Error(),
189 json,
190 json,
191 json.api_request_messageId
192 )
193 );
194 }
195 } else if (isEmpty(json.api_result_data)) {
196 // Empty result_data here means our data isn't ready, wait 1 to 5 seconds and try again
197 debug("Result not ready for " + messageIds);
198 return scheduleRetry();
199 } else {
200 const messageId = json.api_result_data.api_request_messageid;
201 debug("Got result for ", messageId);
202 if (json.api_result_data.hasOwnProperty("api_paging_last_page")) {
203 return resolve(messageId, {
204 lastPage: json.api_result_data.api_paging_last_page,
205 result: json.api_result_data.api_result_data
206 });
207 }
208 return resolve(messageId, json.api_result_data.api_result_data);
209 }
210 });
211 });
212 };
213};
214
215module.exports = BuzzAPI;