1 | const https = require("https");
|
2 | const isEmpty = require("lodash.isempty");
|
3 | const debug = require("debug")("buzzapi");
|
4 | const delay = require("delay");
|
5 | const hyperid = require("hyperid")({ fixedLength: true, urlSafe: true });
|
6 | const os = require("os");
|
7 | const BuzzAPIError = require("./buzzApiError");
|
8 | const pRetry = require("p-retry");
|
9 | const pThrottle = require("p-throttle");
|
10 | const { default: PQueue } = require("p-queue");
|
11 |
|
12 | const 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 |
|
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 |
|
215 | module.exports = BuzzAPI;
|