-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathModel.js
250 lines (197 loc) · 6.53 KB
/
Model.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/* eslint-disable no-console */
/**
* Model constructor
* @author naeemo
*/
import originSuperAgent from "superagent/lib/client";
import cache from "./cache";
// which direction to escape
const ESCAPE_DIRECTIONS = {
before: 1,
after: 2,
both: 3
};
let superAgent = cache(originSuperAgent);
/**
* Manually escape interceptors
* @returns {superAgent.Request}
*/
superAgent.Request.prototype.escape = function (direction = "both") {
if (!(direction in ESCAPE_DIRECTIONS)) {
throw new Error(
"request.escape() called with a unrecognized value: " +
direction +
".\n 'before', 'after', 'both' are supported."
);
}
this._escape = ESCAPE_DIRECTIONS[direction];
return this;
};
/**
* Mark a request as singleton
* this means only one request can be in process at the same time, previous request will be aborted.
* @return {superAgent.Request}
*/
superAgent.Request.prototype.singleton = function () {
this._singleton = true;
return this;
};
/**
* Wrap Promise's resolve/reject, when a singleton request is done, the corresponding record can be cleared.
* @param model
* @param key
* @param promiseHandler
* @return {Function}
*/
function attachSingletonHook(model, key, promiseHandler) {
return function (valueOrErr) { // only one argument will be handled according to Promise spec
model._singletonRequests[key] = null;
promiseHandler(valueOrErr);
};
}
/**
* run beforeInterceptors
* 前置拦截器
* wait for next() call: next(true) continue, next(false) break;
*/
function beforeHook(_request, ...beforeArr) {
return new Promise(function (resolve, reject) {
let len = beforeArr.length;
function next(ok = true, msg) {
!ok && reject(msg);
if (len--) {
if (!beforeArr[len]) return next();
beforeArr[len].call(_request, next);
} else {
resolve();
}
}
if (_request._escape === ESCAPE_DIRECTIONS.before || _request._escape === ESCAPE_DIRECTIONS.both)
resolve();
else {
next();
}
});
}
/**
* run afterInterceptors
* 后置过滤器
* 拦截器显式返回一个false, 则中止循环。
*/
function afterHook(resolve, reject, ...afterArr) {
return function (err, res) {
const _request = this;
if (_request._escape !== ESCAPE_DIRECTIONS.after && _request._escape !== ESCAPE_DIRECTIONS.both) {
let len = afterArr.length;
while (len--) {
// skip invalid ones
if (typeof afterArr[len] !== "function") continue;
const result = afterArr[len].call(_request, err, res);
// 拦截器显式返回一个false, 则中止请求。
if (typeof result !== "undefined" && !result) {
reject(err || res);
// stop the hook calling too
return;
}
}
}
err ? reject(err) : resolve(res);
};
}
/**
* model:
* _base
* request as tool
* api
*
*/
class Model {
constructor(
{
base = "", // string
beforeEach, // function, before request
afterEach, // function, filter response
api = {} // object, all requests of a model
}
) {
const model = this;
/**
* a map to remember ongoing singleton requests
* 正在请求中的单例请求的记录
* @type {Object<String, superAgent.Request>}
* @private
*/
model._singletonRequests = {};
/**
* api server's base url
* api 服务的url前缀,默认使用Model上的
*/
if (typeof base !== "string") {
throw new Error("base url 必须是字符串。");
}
model._base = base || Model.base;
/**
* save the modified request
*/
model.request = superAgent;
// attach the APIs
for (let key in api) {
// api[key];
model[key] = function (...args) {
return new Promise(function (resolve, reject) {
const _request = api[key].apply(model, args);
// abort previous call to api[key]
if (_request._singleton && model._singletonRequests[key]) {
// The abort call will only abort the request, not the Promise wrapping it.
// Promises cannot be aborted, so it would be left as 'pending'.
// This is a memory leak, and I did not know what to do...
model._singletonRequests[key].abort();
model._singletonRequests[key] = null;
}
// wire the base url if necessary:
// any string do not begin with 'http(s)://' or '//' will be wired
if (!/^((https?:)?\/\/)/.test(_request.url)) {
_request.url = model._base + _request.url;
}
beforeHook(_request, beforeEach, Model.beforeEach).then(() => {
// remember singleton request
if (_request._singleton) {
model._singletonRequests[key] = _request;
resolve = attachSingletonHook(model, key, resolve);
reject = attachSingletonHook(model, key, reject);
}
// make request
_request.end(afterHook(resolve, reject, Model.afterEach, afterEach).bind(_request));
}).catch(e => {
console.error(e, ", before requesting " + _request.url);
reject(e);
});
});
};
}
}
/**
* 配置base_url 和 拦截器
* @param base
* @param beforeEach
* @param afterEach
*/
static use(
{
base = "", // string
beforeEach, // function, before request
afterEach // function, filter response
}
) {
if (typeof base !== "string") {
throw new Error("base url 必须是字符串。");
}
Model.base = base;
Model.beforeEach = beforeEach;
Model.afterEach = afterEach;
}
}
Model.base = "";
Model.beforeEach = undefined;
Model.afterEach = undefined;
export default Model