@miso.ai/client-sdk-core 1.14.0-beta.2 → 1.14.0-beta.4
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/cjs/api/ask.js +78 -2
- package/dist/cjs/api/base.js +12 -0
- package/dist/cjs/api/helpers.js +65 -4
- package/dist/cjs/api/history.js +4 -4
- package/dist/cjs/api/iterable.js +23 -8
- package/dist/cjs/classes.js +2 -1
- package/dist/cjs/client.js +2 -1
- package/dist/cjs/plugin/api-recovery.js +123 -0
- package/dist/cjs/plugin/index.js +7 -0
- package/dist/cjs/plugin/plugin-root.js +7 -1
- package/dist/cjs/root.js +1 -0
- package/dist/cjs/version.js +1 -1
- package/package.json +2 -2
- package/src/api/ask.js +72 -3
- package/src/api/base.js +7 -0
- package/src/api/helpers.js +40 -1
- package/src/api/history.js +4 -4
- package/src/api/iterable.js +21 -6
- package/src/plugin/CLAUDE.md +18 -0
- package/src/plugin/api-recovery.js +124 -0
- package/src/plugin/index.js +1 -0
- package/src/plugin/plugin-root.js +8 -0
- package/src/root.js +1 -0
- package/src/version.js +1 -1
package/dist/cjs/api/ask.js
CHANGED
|
@@ -42,7 +42,11 @@ class Ask extends _base.default {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Retrieve multiple question-answer pairs at once
|
|
45
|
+
* Retrieve multiple question-answer pairs at once: an iterable that polls
|
|
46
|
+
* the answers endpoint, like questions(). The id set is mutable between
|
|
47
|
+
* polls: `question_ids` may be a function returning the id array, resolved
|
|
48
|
+
* for every poll, and a fixed initial array settles out record by record
|
|
49
|
+
* (see Answers).
|
|
46
50
|
*/
|
|
47
51
|
async answers(payload, options = {}) {
|
|
48
52
|
if (Array.isArray(payload)) {
|
|
@@ -50,7 +54,9 @@ class Ask extends _base.default {
|
|
|
50
54
|
question_ids: payload
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
|
-
return this
|
|
57
|
+
return new Answers(this, payload, Object.assign({
|
|
58
|
+
immediate: true
|
|
59
|
+
}, options));
|
|
54
60
|
}
|
|
55
61
|
async relatedQuestions(payload, options = {}) {
|
|
56
62
|
return this._run(NAME.RELATED_QUESTIONS, payload, options);
|
|
@@ -95,6 +101,76 @@ class Answer extends _iterable.IdBasedIterableApiStub {
|
|
|
95
101
|
return this._id;
|
|
96
102
|
}
|
|
97
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The answer contents of a set of questions, polled until settled — the id
|
|
107
|
+
* set is mutable between polls, and the stream ends when it runs dry. The
|
|
108
|
+
* question ids may be given as a function returning the id array, resolved
|
|
109
|
+
* for every poll (settled ids drop out, new ones join, at the provider's
|
|
110
|
+
* discretion); a fixed initial id array is mutable all the same — its ids
|
|
111
|
+
* settle out as their records come back finished (_settle). Each fetch's
|
|
112
|
+
* issue order acts as the response revision, so a late, outdated response
|
|
113
|
+
* is discarded by the polling.
|
|
114
|
+
*/
|
|
115
|
+
class Answers extends _iterable.IterableApiStub {
|
|
116
|
+
constructor(api, payload, options = {}) {
|
|
117
|
+
super(mergeCustomApiIterator(api, options));
|
|
118
|
+
this._api = api;
|
|
119
|
+
this._payload = payload;
|
|
120
|
+
const {
|
|
121
|
+
question_ids
|
|
122
|
+
} = payload;
|
|
123
|
+
this._pending = typeof question_ids === 'function' ? undefined : new Set(question_ids || []);
|
|
124
|
+
}
|
|
125
|
+
async _get(options) {
|
|
126
|
+
// a plain one-shot read of the current ids' answers
|
|
127
|
+
return this._api._run(NAME.ANSWERS, Object.assign({}, this._payload, {
|
|
128
|
+
question_ids: this._questionIds()
|
|
129
|
+
}), options);
|
|
130
|
+
}
|
|
131
|
+
_questionIds() {
|
|
132
|
+
return this._pending ? [...this._pending] : this._payload.question_ids();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// a fixed id set settles by the records themselves: a record that came
|
|
136
|
+
// back with its finished answer needs no further polling
|
|
137
|
+
_settle(response) {
|
|
138
|
+
if (!this._pending || !Array.isArray(response)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const record of response) {
|
|
142
|
+
if (record && record.answer !== undefined && record.finished !== false) {
|
|
143
|
+
this._pending.delete(record.question_id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
_fetch() {
|
|
148
|
+
let index = 0;
|
|
149
|
+
return async ({
|
|
150
|
+
signal
|
|
151
|
+
} = {}) => {
|
|
152
|
+
const i = ++index; // taken at issue time: a stale response resolves with a lower revision
|
|
153
|
+
const question_ids = this._questionIds();
|
|
154
|
+
if (!question_ids || !question_ids.length) {
|
|
155
|
+
return [undefined, true, i]; // nothing left to fetch; the stream ends
|
|
156
|
+
}
|
|
157
|
+
// TODO: use single answer endpoint when array length = 1
|
|
158
|
+
const response = await this._api._run(NAME.ANSWERS, Object.assign({}, this._payload, {
|
|
159
|
+
question_ids
|
|
160
|
+
}), {
|
|
161
|
+
signal
|
|
162
|
+
});
|
|
163
|
+
this._settle(response);
|
|
164
|
+
return [response, false, i];
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// any response is progress for the stall timeout: the record-diff test of
|
|
169
|
+
// the base class reads a single answer's shape, not an array of records
|
|
170
|
+
_isUpdated() {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
98
174
|
class SearchResult extends _iterable.IdBasedIterableApiStub {
|
|
99
175
|
constructor(api, response, options = {}) {
|
|
100
176
|
super(api, '_searchGet', response.question_id, mergeCustomApiIterator(api, options));
|
package/dist/cjs/api/base.js
CHANGED
|
@@ -16,6 +16,18 @@ class ApiBase extends _commons.Component {
|
|
|
16
16
|
this.context = api._client.context;
|
|
17
17
|
}
|
|
18
18
|
async _run(apiName, payload, options = {}) {
|
|
19
|
+
const apiGroup = this._apiPath;
|
|
20
|
+
return this.helpers.applyMiddlewares({
|
|
21
|
+
apiGroup,
|
|
22
|
+
apiName,
|
|
23
|
+
payload,
|
|
24
|
+
options
|
|
25
|
+
}, () => this._request(apiName, payload, options));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// one round of the request: the passes run per round, so a round run again
|
|
29
|
+
// by a middleware picks up the context as of then (e.g. a renewed token)
|
|
30
|
+
async _request(apiName, payload, options) {
|
|
19
31
|
const {
|
|
20
32
|
bulk
|
|
21
33
|
} = options;
|
package/dist/cjs/api/helpers.js
CHANGED
|
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _commons = require("@miso.ai/commons");
|
|
8
8
|
var _constants = require("../constants.js");
|
|
9
|
-
const _excluded = ["method", "headers", "timeout"]
|
|
9
|
+
const _excluded = ["method", "headers", "timeout", "retry"],
|
|
10
|
+
_excluded2 = ["method", "headers", "timeout", "body"];
|
|
10
11
|
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
11
12
|
class ApiHelpers {
|
|
12
13
|
constructor(client, root) {
|
|
@@ -18,18 +19,50 @@ class ApiHelpers {
|
|
|
18
19
|
let {
|
|
19
20
|
method = 'POST',
|
|
20
21
|
headers,
|
|
21
|
-
timeout
|
|
22
|
+
timeout,
|
|
23
|
+
retry
|
|
22
24
|
} = _ref,
|
|
23
25
|
options = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
24
26
|
const {
|
|
25
|
-
apiKey,
|
|
26
27
|
request = {}
|
|
27
28
|
} = this._client.options;
|
|
28
29
|
timeout = timeout || request.timeout;
|
|
30
|
+
// retries on top of the first attempt, for failures that may be
|
|
31
|
+
// transient — network errors, timeouts, and server (5xx) errors;
|
|
32
|
+
// client (4xx) and application errors are deterministic, not retried.
|
|
33
|
+
// Off by default: a retried POST is not safe for non-idempotent calls
|
|
34
|
+
// (e.g. posting a question), so it is opted in per request via the
|
|
35
|
+
// request options, or client-wide via `options.request.retry`
|
|
36
|
+
retry = retry !== undefined ? retry : request.retry || 0;
|
|
29
37
|
|
|
30
38
|
// TODO: organize arguments
|
|
31
39
|
const body = method !== 'GET' && payload != undefined ? JSON.stringify(payload) : undefined;
|
|
40
|
+
for (let attempt = 0;; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
return await this._fetch(url, Object.assign({
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
timeout,
|
|
46
|
+
body
|
|
47
|
+
}, options));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (attempt >= retry || !isRetryableFetchError(error)) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
32
55
|
|
|
56
|
+
// one attempt; the timeout signal is created per attempt, so a retried
|
|
57
|
+
// attempt gets a full window of its own
|
|
58
|
+
async _fetch(url, _ref2) {
|
|
59
|
+
let {
|
|
60
|
+
method,
|
|
61
|
+
headers,
|
|
62
|
+
timeout,
|
|
63
|
+
body
|
|
64
|
+
} = _ref2,
|
|
65
|
+
options = _objectWithoutPropertiesLoose(_ref2, _excluded2);
|
|
33
66
|
// TODO: merge signal from options if any
|
|
34
67
|
const signal = timeout ? AbortSignal.timeout(timeout) : undefined;
|
|
35
68
|
const res = await (this._root._customFetch || fetch)(url, (0, _commons.trimObj)(Object.assign({
|
|
@@ -131,6 +164,27 @@ class ApiHelpers {
|
|
|
131
164
|
} = this._client.options;
|
|
132
165
|
return apiName === 'interactions' ? eventEndpoint : dataEndpoint;
|
|
133
166
|
}
|
|
167
|
+
|
|
168
|
+
// middlewares wrap the whole request — passes, send, and postprocess —
|
|
169
|
+
// in registration order, the first being the outermost. Each is called
|
|
170
|
+
// with the request info and a `next` function that runs the rest of the
|
|
171
|
+
// chain, which it may defer, catch the failure of, or call again
|
|
172
|
+
async applyMiddlewares({
|
|
173
|
+
apiGroup,
|
|
174
|
+
apiName,
|
|
175
|
+
payload,
|
|
176
|
+
options
|
|
177
|
+
}, run) {
|
|
178
|
+
const client = this._client;
|
|
179
|
+
const request = Object.freeze({
|
|
180
|
+
client,
|
|
181
|
+
apiGroup,
|
|
182
|
+
apiName,
|
|
183
|
+
payload,
|
|
184
|
+
options
|
|
185
|
+
});
|
|
186
|
+
return this._root._apiMiddlewares.reduceRight((next, middleware) => () => middleware(request, next), run)();
|
|
187
|
+
}
|
|
134
188
|
applyUrlPasses(component, {
|
|
135
189
|
apiGroup,
|
|
136
190
|
apiName,
|
|
@@ -196,4 +250,11 @@ class ApiHelpers {
|
|
|
196
250
|
return headers;
|
|
197
251
|
}
|
|
198
252
|
}
|
|
199
|
-
|
|
253
|
+
|
|
254
|
+
// no status: a network failure, a timeout, or an unparsable body — all
|
|
255
|
+
// transport-level; 5xx: the server's own failure. A 4xx or an
|
|
256
|
+
// application-level error (2xx with `errors`) is deterministic
|
|
257
|
+
exports.default = ApiHelpers;
|
|
258
|
+
function isRetryableFetchError(error) {
|
|
259
|
+
return error.status === undefined || error.status >= 500;
|
|
260
|
+
}
|
package/dist/cjs/api/history.js
CHANGED
|
@@ -14,13 +14,13 @@ class UserHistory extends _base.default {
|
|
|
14
14
|
constructor(api) {
|
|
15
15
|
super(api, GROUP.ASK_USER_HISTORY);
|
|
16
16
|
}
|
|
17
|
-
async getThreads(options) {
|
|
18
|
-
return this._run('threads',
|
|
17
|
+
async getThreads(payload, options) {
|
|
18
|
+
return this._run('threads', payload, Object.assign({}, options, {
|
|
19
19
|
method: 'GET'
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
|
-
async getThread(threadId, options) {
|
|
23
|
-
return this._run(`threads/${threadId}`,
|
|
22
|
+
async getThread(threadId, payload, options) {
|
|
23
|
+
return this._run(`threads/${threadId}`, payload, Object.assign({}, options, {
|
|
24
24
|
method: 'GET'
|
|
25
25
|
}));
|
|
26
26
|
}
|
package/dist/cjs/api/iterable.js
CHANGED
|
@@ -22,13 +22,7 @@ class IterableApiStub {
|
|
|
22
22
|
this._ac.abort(reason);
|
|
23
23
|
}
|
|
24
24
|
[Symbol.asyncIterator]() {
|
|
25
|
-
const fetch =
|
|
26
|
-
signal
|
|
27
|
-
} = {}) => {
|
|
28
|
-
// TODO: pass signal
|
|
29
|
-
const response = await this.get();
|
|
30
|
-
return [response, isFinished(response), revisionOf(response)];
|
|
31
|
-
};
|
|
25
|
+
const fetch = this._fetch();
|
|
32
26
|
if (typeof this._options.customIterator === 'function') {
|
|
33
27
|
return this._options.customIterator.call(this, fetch);
|
|
34
28
|
}
|
|
@@ -54,7 +48,7 @@ class IterableApiStub {
|
|
|
54
48
|
const onResponse = (response, finished) => {
|
|
55
49
|
if (finished) {
|
|
56
50
|
stac.clear();
|
|
57
|
-
} else if (
|
|
51
|
+
} else if (this._isUpdated(prevResponse, response)) {
|
|
58
52
|
stac.touch();
|
|
59
53
|
}
|
|
60
54
|
prevResponse = response;
|
|
@@ -65,6 +59,27 @@ class IterableApiStub {
|
|
|
65
59
|
onResponse
|
|
66
60
|
}, options))[Symbol.asyncIterator]();
|
|
67
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compose the polling fetch: () => [response, finished, revision].
|
|
65
|
+
* Overridable by a subclass with its own finish or revision semantics
|
|
66
|
+
* (e.g. Answers, whose question ids may run dry).
|
|
67
|
+
*/
|
|
68
|
+
_fetch() {
|
|
69
|
+
return async ({
|
|
70
|
+
signal
|
|
71
|
+
} = {}) => {
|
|
72
|
+
// TODO: pass signal
|
|
73
|
+
const response = await this.get();
|
|
74
|
+
return [response, isFinished(response), revisionOf(response)];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// whether the new response carries progress, keeping the stall timeout at
|
|
79
|
+
// bay; overridable by a subclass with its own response shape
|
|
80
|
+
_isUpdated(previousResponse, newResponse) {
|
|
81
|
+
return isUpdated(previousResponse, newResponse);
|
|
82
|
+
}
|
|
68
83
|
}
|
|
69
84
|
exports.IterableApiStub = IterableApiStub;
|
|
70
85
|
class IdBasedIterableApiStub extends IterableApiStub {
|
package/dist/cjs/classes.js
CHANGED
|
@@ -7,7 +7,8 @@ exports.default = void 0;
|
|
|
7
7
|
var api = _interopRequireWildcard(require("./api/index.js"));
|
|
8
8
|
var _context = _interopRequireDefault(require("./context.js"));
|
|
9
9
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
-
|
|
10
|
+
var cache;
|
|
11
|
+
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
|
11
12
|
var _default = exports.default = {
|
|
12
13
|
api,
|
|
13
14
|
context: {
|
package/dist/cjs/client.js
CHANGED
|
@@ -9,7 +9,8 @@ var _root = require("./root.js");
|
|
|
9
9
|
var _index = require("./api/index.js");
|
|
10
10
|
var _context = _interopRequireDefault(require("./context.js"));
|
|
11
11
|
var helpers = _interopRequireWildcard(require("./utils.js"));
|
|
12
|
-
|
|
12
|
+
var cache;
|
|
13
|
+
function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var r, c, a = Object.defineProperty, n = { __proto__: null, default: e }; if (Object(e) !== e) return n; if (cache || "function" != typeof WeakMap || (cache = new WeakMap()), cache) { if (cache.has(e)) return cache.get(e); cache.set(e, n); } for (c in e) "default" !== c && {}.hasOwnProperty.call(e, c) && ((r = a && Object.getOwnPropertyDescriptor(e, c)) && (r.get || r.set) ? a(n, c, r) : n[c] = e[c]); return n; }
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
15
|
const currentScript = _commons.isInBrowser ? document.currentScript : undefined;
|
|
15
16
|
class MisoClient extends _commons.Component {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _commons = require("@miso.ai/commons");
|
|
8
|
+
const _excluded = ["client"];
|
|
9
|
+
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
|
|
10
|
+
const PLUGIN_ID = 'std:api-recovery';
|
|
11
|
+
|
|
12
|
+
// a safety net against a callback that resends unconditionally
|
|
13
|
+
const MAX_RESENT = 10;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lets the application intercept failed API requests, per client:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* client.api.onError = async (error, { request, hold, resend }) => { ... };
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The request settles with the outcome of the callback: the error it throws,
|
|
23
|
+
* the value it returns (taken as the API result), or — by returning
|
|
24
|
+
* `resend()` — the outcome of sending the request again.
|
|
25
|
+
*
|
|
26
|
+
* A resent request that fails is intercepted like any other, with
|
|
27
|
+
* `error.resent` telling how many times the request has been resent so far
|
|
28
|
+
* (0 for the original failure), so the callback can give up at some count.
|
|
29
|
+
* As a safety net, a request resent 10 times fails for good, its error
|
|
30
|
+
* thrown without reaching the callback.
|
|
31
|
+
*
|
|
32
|
+
* `hold()` holds the requests issued from then on (resent ones included)
|
|
33
|
+
* until the unhold function it returns is called. Requests already in flight
|
|
34
|
+
* are not affected, so several errors may reach the callback before a hold
|
|
35
|
+
* takes effect: managing them in one go (e.g. sharing a single token renewal)
|
|
36
|
+
* is up to the application.
|
|
37
|
+
*/
|
|
38
|
+
class ApiRecoveryPlugin {
|
|
39
|
+
static get id() {
|
|
40
|
+
return PLUGIN_ID;
|
|
41
|
+
}
|
|
42
|
+
constructor() {
|
|
43
|
+
this._recoveries = new WeakMap();
|
|
44
|
+
}
|
|
45
|
+
install(MisoClient, context) {
|
|
46
|
+
context.addApiMiddleware(this._middleware.bind(this));
|
|
47
|
+
MisoClient.on('create', this._injectClient.bind(this));
|
|
48
|
+
}
|
|
49
|
+
_injectClient(client) {
|
|
50
|
+
const recovery = new ApiRecovery();
|
|
51
|
+
this._recoveries.set(client, recovery);
|
|
52
|
+
(0, _commons.delegateProperties)(client.api, recovery, ['onError']);
|
|
53
|
+
}
|
|
54
|
+
async _middleware(_ref, next) {
|
|
55
|
+
let {
|
|
56
|
+
client
|
|
57
|
+
} = _ref,
|
|
58
|
+
request = _objectWithoutPropertiesLoose(_ref, _excluded);
|
|
59
|
+
const recovery = this._recoveries.get(client);
|
|
60
|
+
return recovery ? recovery.run(request, next) : next();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.default = ApiRecoveryPlugin;
|
|
64
|
+
class ApiRecovery {
|
|
65
|
+
constructor() {
|
|
66
|
+
this._callback = undefined;
|
|
67
|
+
this._holds = new Set();
|
|
68
|
+
this._gate = undefined;
|
|
69
|
+
}
|
|
70
|
+
get onError() {
|
|
71
|
+
return this._callback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// a single callback: setting it replaces the current one, and setting it
|
|
75
|
+
// to undefined removes it
|
|
76
|
+
set onError(callback) {
|
|
77
|
+
if (callback != undefined && typeof callback !== 'function') {
|
|
78
|
+
throw new Error(`Expect onError to be a function: ${callback}`);
|
|
79
|
+
}
|
|
80
|
+
this._callback = callback || undefined;
|
|
81
|
+
}
|
|
82
|
+
async run(request, next, resent = 0) {
|
|
83
|
+
try {
|
|
84
|
+
return await this._send(next);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const callback = this._callback;
|
|
87
|
+
if (!callback || resent >= MAX_RESENT) {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (error && typeof error === 'object') {
|
|
91
|
+
error.resent = resent;
|
|
92
|
+
}
|
|
93
|
+
return callback(error, Object.freeze({
|
|
94
|
+
request,
|
|
95
|
+
hold: () => this._hold(),
|
|
96
|
+
resend: () => this.run(request, next, resent + 1)
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async _send(next) {
|
|
101
|
+
// loop: a new hold may be placed by the time the gate opens
|
|
102
|
+
while (this._gate) {
|
|
103
|
+
await this._gate.promise;
|
|
104
|
+
}
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
_hold() {
|
|
108
|
+
const hold = {};
|
|
109
|
+
this._holds.add(hold);
|
|
110
|
+
if (!this._gate) {
|
|
111
|
+
this._gate = new _commons.Resolution();
|
|
112
|
+
}
|
|
113
|
+
// holds overlap: the gate opens when the last one is released
|
|
114
|
+
return () => {
|
|
115
|
+
if (!this._holds.delete(hold) || this._holds.size) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const gate = this._gate;
|
|
119
|
+
this._gate = undefined;
|
|
120
|
+
gate.resolve();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/cjs/plugin/index.js
CHANGED
|
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "ApiPatchPlugin", {
|
|
|
15
15
|
return _apiPatch.default;
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
|
+
Object.defineProperty(exports, "ApiRecoveryPlugin", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _apiRecovery.default;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
18
24
|
Object.defineProperty(exports, "AutoEventsPlugin", {
|
|
19
25
|
enumerable: true,
|
|
20
26
|
get: function () {
|
|
@@ -57,6 +63,7 @@ var _autoEvents = _interopRequireDefault(require("./auto-events.js"));
|
|
|
57
63
|
var _interactions = _interopRequireDefault(require("./interactions.js"));
|
|
58
64
|
var _nativeFetch = _interopRequireDefault(require("./native-fetch.js"));
|
|
59
65
|
var _apiPatch = _interopRequireDefault(require("./api-patch.js"));
|
|
66
|
+
var _apiRecovery = _interopRequireDefault(require("./api-recovery.js"));
|
|
60
67
|
var _headerApiKey = _interopRequireDefault(require("./header-api-key.js"));
|
|
61
68
|
var _analytics = _interopRequireDefault(require("./analytics.js"));
|
|
62
69
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -106,6 +106,12 @@ class PluginRoot extends _commons.Registry {
|
|
|
106
106
|
}
|
|
107
107
|
this._root._urlPasses.push(pass);
|
|
108
108
|
}
|
|
109
|
+
addApiMiddleware(middleware) {
|
|
110
|
+
if (typeof middleware !== 'function') {
|
|
111
|
+
throw new Error(`Expect parameter to be a function: ${middleware}`);
|
|
112
|
+
}
|
|
113
|
+
this._root._apiMiddlewares.push(middleware);
|
|
114
|
+
}
|
|
109
115
|
setCustomFetch(fetch) {
|
|
110
116
|
if (typeof fetch !== 'function') {
|
|
111
117
|
throw new Error(`Expect parameter to be a function: ${fetch}`);
|
|
@@ -253,7 +259,7 @@ const PLUGIN_QUERY_METHODS = ['installed', 'registered', 'isInstalled', 'isRegis
|
|
|
253
259
|
*/
|
|
254
260
|
class PluginContext {
|
|
255
261
|
constructor(root, classes) {
|
|
256
|
-
(0, _commons.delegateGetters)(this, root, [...PLUGIN_QUERY_METHODS, 'getPluginClass', 'addSubtree', 'addExtensionPoints', 'addPayloadPass', 'addHeadersPass', 'addUrlPass', 'setCustomFetch', 'setCustomSendBeacon', 'setCustomApiIterator', 'onHubUpdate', 'onHubEmit']);
|
|
262
|
+
(0, _commons.delegateGetters)(this, root, [...PLUGIN_QUERY_METHODS, 'getPluginClass', 'addSubtree', 'addExtensionPoints', 'addPayloadPass', 'addHeadersPass', 'addUrlPass', 'addApiMiddleware', 'setCustomFetch', 'setCustomSendBeacon', 'setCustomApiIterator', 'onHubUpdate', 'onHubEmit']);
|
|
257
263
|
(0, _commons.defineValues)(this, {
|
|
258
264
|
classes
|
|
259
265
|
});
|
package/dist/cjs/root.js
CHANGED
package/dist/cjs/version.js
CHANGED
package/package.json
CHANGED
package/src/api/ask.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { API, defineValues, trimObj } from '@miso.ai/commons';
|
|
2
2
|
import ApiBase from './base.js';
|
|
3
3
|
import UserHistoryV0 from './history.0.js';
|
|
4
|
-
import { IdBasedIterableApiStub } from './iterable.js';
|
|
4
|
+
import { IterableApiStub, IdBasedIterableApiStub } from './iterable.js';
|
|
5
5
|
|
|
6
6
|
const { GROUP, NAME } = API;
|
|
7
7
|
|
|
@@ -34,13 +34,17 @@ export default class Ask extends ApiBase {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Retrieve multiple question-answer pairs at once
|
|
37
|
+
* Retrieve multiple question-answer pairs at once: an iterable that polls
|
|
38
|
+
* the answers endpoint, like questions(). The id set is mutable between
|
|
39
|
+
* polls: `question_ids` may be a function returning the id array, resolved
|
|
40
|
+
* for every poll, and a fixed initial array settles out record by record
|
|
41
|
+
* (see Answers).
|
|
38
42
|
*/
|
|
39
43
|
async answers(payload, options = {}) {
|
|
40
44
|
if (Array.isArray(payload)) {
|
|
41
45
|
payload = { question_ids: payload };
|
|
42
46
|
}
|
|
43
|
-
return this
|
|
47
|
+
return new Answers(this, payload, { immediate: true, ...options });
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
async relatedQuestions(payload, options = {}) {
|
|
@@ -93,6 +97,71 @@ class Answer extends IdBasedIterableApiStub {
|
|
|
93
97
|
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The answer contents of a set of questions, polled until settled — the id
|
|
102
|
+
* set is mutable between polls, and the stream ends when it runs dry. The
|
|
103
|
+
* question ids may be given as a function returning the id array, resolved
|
|
104
|
+
* for every poll (settled ids drop out, new ones join, at the provider's
|
|
105
|
+
* discretion); a fixed initial id array is mutable all the same — its ids
|
|
106
|
+
* settle out as their records come back finished (_settle). Each fetch's
|
|
107
|
+
* issue order acts as the response revision, so a late, outdated response
|
|
108
|
+
* is discarded by the polling.
|
|
109
|
+
*/
|
|
110
|
+
class Answers extends IterableApiStub {
|
|
111
|
+
|
|
112
|
+
constructor(api, payload, options = {}) {
|
|
113
|
+
super(mergeCustomApiIterator(api, options));
|
|
114
|
+
this._api = api;
|
|
115
|
+
this._payload = payload;
|
|
116
|
+
const { question_ids } = payload;
|
|
117
|
+
this._pending = typeof question_ids === 'function' ? undefined : new Set(question_ids || []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _get(options) {
|
|
121
|
+
// a plain one-shot read of the current ids' answers
|
|
122
|
+
return this._api._run(NAME.ANSWERS, { ...this._payload, question_ids: this._questionIds() }, options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_questionIds() {
|
|
126
|
+
return this._pending ? [...this._pending] : this._payload.question_ids();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// a fixed id set settles by the records themselves: a record that came
|
|
130
|
+
// back with its finished answer needs no further polling
|
|
131
|
+
_settle(response) {
|
|
132
|
+
if (!this._pending || !Array.isArray(response)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const record of response) {
|
|
136
|
+
if (record && record.answer !== undefined && record.finished !== false) {
|
|
137
|
+
this._pending.delete(record.question_id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_fetch() {
|
|
143
|
+
let index = 0;
|
|
144
|
+
return async ({ signal } = {}) => {
|
|
145
|
+
const i = ++index; // taken at issue time: a stale response resolves with a lower revision
|
|
146
|
+
const question_ids = this._questionIds();
|
|
147
|
+
if (!question_ids || !question_ids.length) {
|
|
148
|
+
return [undefined, true, i]; // nothing left to fetch; the stream ends
|
|
149
|
+
}
|
|
150
|
+
// TODO: use single answer endpoint when array length = 1
|
|
151
|
+
const response = await this._api._run(NAME.ANSWERS, { ...this._payload, question_ids }, { signal });
|
|
152
|
+
this._settle(response);
|
|
153
|
+
return [response, false, i];
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// any response is progress for the stall timeout: the record-diff test of
|
|
158
|
+
// the base class reads a single answer's shape, not an array of records
|
|
159
|
+
_isUpdated() {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
96
165
|
class SearchResult extends IdBasedIterableApiStub {
|
|
97
166
|
|
|
98
167
|
constructor(api, response, options = {}) {
|
package/src/api/base.js
CHANGED
|
@@ -11,6 +11,13 @@ export default class ApiBase extends Component {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async _run(apiName, payload, options = {}) {
|
|
14
|
+
const apiGroup = this._apiPath;
|
|
15
|
+
return this.helpers.applyMiddlewares({ apiGroup, apiName, payload, options }, () => this._request(apiName, payload, options));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// one round of the request: the passes run per round, so a round run again
|
|
19
|
+
// by a middleware picks up the context as of then (e.g. a renewed token)
|
|
20
|
+
async _request(apiName, payload, options) {
|
|
14
21
|
const { bulk } = options;
|
|
15
22
|
const bulkInfo = bulk ? { bulk: this.helpers.bulkInfo } : undefined;
|
|
16
23
|
const apiGroup = this._apiPath;
|
package/src/api/helpers.js
CHANGED
|
@@ -13,14 +13,36 @@ export default class ApiHelpers {
|
|
|
13
13
|
method = 'POST',
|
|
14
14
|
headers,
|
|
15
15
|
timeout,
|
|
16
|
+
retry,
|
|
16
17
|
...options
|
|
17
18
|
} = {}) {
|
|
18
|
-
const {
|
|
19
|
+
const { request = {} } = this._client.options;
|
|
19
20
|
timeout = timeout || request.timeout;
|
|
21
|
+
// retries on top of the first attempt, for failures that may be
|
|
22
|
+
// transient — network errors, timeouts, and server (5xx) errors;
|
|
23
|
+
// client (4xx) and application errors are deterministic, not retried.
|
|
24
|
+
// Off by default: a retried POST is not safe for non-idempotent calls
|
|
25
|
+
// (e.g. posting a question), so it is opted in per request via the
|
|
26
|
+
// request options, or client-wide via `options.request.retry`
|
|
27
|
+
retry = retry !== undefined ? retry : (request.retry || 0);
|
|
20
28
|
|
|
21
29
|
// TODO: organize arguments
|
|
22
30
|
const body = method !== 'GET' && payload != undefined ? JSON.stringify(payload) : undefined;
|
|
23
31
|
|
|
32
|
+
for (let attempt = 0; ; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
return await this._fetch(url, { method, headers, timeout, body, ...options });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (attempt >= retry || !isRetryableFetchError(error)) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// one attempt; the timeout signal is created per attempt, so a retried
|
|
44
|
+
// attempt gets a full window of its own
|
|
45
|
+
async _fetch(url, { method, headers, timeout, body, ...options }) {
|
|
24
46
|
// TODO: merge signal from options if any
|
|
25
47
|
const signal = timeout ? AbortSignal.timeout(timeout) : undefined;
|
|
26
48
|
|
|
@@ -106,6 +128,16 @@ export default class ApiHelpers {
|
|
|
106
128
|
return apiName === 'interactions' ? eventEndpoint : dataEndpoint;
|
|
107
129
|
}
|
|
108
130
|
|
|
131
|
+
// middlewares wrap the whole request — passes, send, and postprocess —
|
|
132
|
+
// in registration order, the first being the outermost. Each is called
|
|
133
|
+
// with the request info and a `next` function that runs the rest of the
|
|
134
|
+
// chain, which it may defer, catch the failure of, or call again
|
|
135
|
+
async applyMiddlewares({ apiGroup, apiName, payload, options }, run) {
|
|
136
|
+
const client = this._client;
|
|
137
|
+
const request = Object.freeze({ client, apiGroup, apiName, payload, options });
|
|
138
|
+
return this._root._apiMiddlewares.reduceRight((next, middleware) => () => middleware(request, next), run)();
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
applyUrlPasses(component, { apiGroup, apiName, url }) {
|
|
110
142
|
const client = this._client;
|
|
111
143
|
for (const pass of this._root._urlPasses) {
|
|
@@ -143,3 +175,10 @@ export default class ApiHelpers {
|
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
}
|
|
178
|
+
|
|
179
|
+
// no status: a network failure, a timeout, or an unparsable body — all
|
|
180
|
+
// transport-level; 5xx: the server's own failure. A 4xx or an
|
|
181
|
+
// application-level error (2xx with `errors`) is deterministic
|
|
182
|
+
function isRetryableFetchError(error) {
|
|
183
|
+
return error.status === undefined || error.status >= 500;
|
|
184
|
+
}
|
package/src/api/history.js
CHANGED
|
@@ -9,12 +9,12 @@ export default class UserHistory extends ApiBase {
|
|
|
9
9
|
super(api, GROUP.ASK_USER_HISTORY);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async getThreads(options) {
|
|
13
|
-
return this._run('threads',
|
|
12
|
+
async getThreads(payload, options) {
|
|
13
|
+
return this._run('threads', payload, { ...options, method: 'GET' });
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
async getThread(threadId, options) {
|
|
17
|
-
return this._run(`threads/${threadId}`,
|
|
16
|
+
async getThread(threadId, payload, options) {
|
|
17
|
+
return this._run(`threads/${threadId}`, payload, { ...options, method: 'GET' });
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async updateThread(threadId, payload, options) {
|
package/src/api/iterable.js
CHANGED
|
@@ -20,11 +20,7 @@ export class IterableApiStub {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
[Symbol.asyncIterator]() {
|
|
23
|
-
const fetch =
|
|
24
|
-
// TODO: pass signal
|
|
25
|
-
const response = await this.get();
|
|
26
|
-
return [response, isFinished(response), revisionOf(response)];
|
|
27
|
-
};
|
|
23
|
+
const fetch = this._fetch();
|
|
28
24
|
|
|
29
25
|
if (typeof this._options.customIterator === 'function') {
|
|
30
26
|
return this._options.customIterator.call(this, fetch);
|
|
@@ -47,7 +43,7 @@ export class IterableApiStub {
|
|
|
47
43
|
const onResponse = (response, finished) => {
|
|
48
44
|
if (finished) {
|
|
49
45
|
stac.clear();
|
|
50
|
-
} else if (
|
|
46
|
+
} else if (this._isUpdated(prevResponse, response)) {
|
|
51
47
|
stac.touch();
|
|
52
48
|
}
|
|
53
49
|
prevResponse = response;
|
|
@@ -55,6 +51,25 @@ export class IterableApiStub {
|
|
|
55
51
|
return polling(fetch, { interval, signal, onResponse, ...options })[Symbol.asyncIterator]();
|
|
56
52
|
}
|
|
57
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Compose the polling fetch: () => [response, finished, revision].
|
|
56
|
+
* Overridable by a subclass with its own finish or revision semantics
|
|
57
|
+
* (e.g. Answers, whose question ids may run dry).
|
|
58
|
+
*/
|
|
59
|
+
_fetch() {
|
|
60
|
+
return async ({ signal } = {}) => {
|
|
61
|
+
// TODO: pass signal
|
|
62
|
+
const response = await this.get();
|
|
63
|
+
return [response, isFinished(response), revisionOf(response)];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// whether the new response carries progress, keeping the stall timeout at
|
|
68
|
+
// bay; overridable by a subclass with its own response shape
|
|
69
|
+
_isUpdated(previousResponse, newResponse) {
|
|
70
|
+
return isUpdated(previousResponse, newResponse);
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
export class IdBasedIterableApiStub extends IterableApiStub {
|
package/src/plugin/CLAUDE.md
CHANGED
|
@@ -20,6 +20,7 @@ Exposed to plugins during `install()`, provides hooks into the SDK:
|
|
|
20
20
|
context.addPayloadPass(fn) // Modify API request payloads
|
|
21
21
|
context.addHeadersPass(fn) // Modify request headers
|
|
22
22
|
context.addUrlPass(fn) // Modify request URLs
|
|
23
|
+
context.addApiMiddleware(fn) // Wrap API requests: defer, catch failures, resend
|
|
23
24
|
context.setCustomFetch(fn) // Override fetch implementation
|
|
24
25
|
context.setCustomSendBeacon(fn) // Override sendBeacon
|
|
25
26
|
context.addSubtree(component) // Register as component subtree for event propagation
|
|
@@ -77,6 +78,7 @@ export default class MyPlugin {
|
|
|
77
78
|
| `InteractionsPlugin` | `std:interactions` | Enriches interaction records with SDK version/UUID |
|
|
78
79
|
| `NativeFetchPlugin` | `std:native-fetch` | Uses native fetch, bypassing custom implementations |
|
|
79
80
|
| `ApiPatchPlugin` | `std:api-patch` | Applies patches to API responses |
|
|
81
|
+
| `ApiRecoveryPlugin` | `std:api-recovery` | `client.api.onError`: intercept failed requests, hold and resend |
|
|
80
82
|
| `HeaderApiKeyPlugin` | `std:header-api-key` | Sends API key in header instead of query |
|
|
81
83
|
| `AnalyticsPlugin` | `std:analytics` | User engagement timing and analytics |
|
|
82
84
|
|
|
@@ -98,6 +100,22 @@ function modifyHeaders({ client, headers }) {
|
|
|
98
100
|
}
|
|
99
101
|
```
|
|
100
102
|
|
|
103
|
+
## API Middlewares
|
|
104
|
+
|
|
105
|
+
A middleware wraps a whole API request (passes, send, postprocess). It may defer the request, catch its failure, or run it again — each round re-applies the passes, so it picks up the context as of then:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// request: { client, apiGroup, apiName, payload, options }
|
|
109
|
+
async function middleware(request, next) {
|
|
110
|
+
try {
|
|
111
|
+
return await next();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// ...
|
|
114
|
+
return next();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
101
119
|
## Lifecycle
|
|
102
120
|
|
|
103
121
|
1. `register()` - Plugin class stored by ID
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Resolution, delegateProperties } from '@miso.ai/commons';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'std:api-recovery';
|
|
4
|
+
|
|
5
|
+
// a safety net against a callback that resends unconditionally
|
|
6
|
+
const MAX_RESENT = 10;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lets the application intercept failed API requests, per client:
|
|
10
|
+
*
|
|
11
|
+
* ```js
|
|
12
|
+
* client.api.onError = async (error, { request, hold, resend }) => { ... };
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The request settles with the outcome of the callback: the error it throws,
|
|
16
|
+
* the value it returns (taken as the API result), or — by returning
|
|
17
|
+
* `resend()` — the outcome of sending the request again.
|
|
18
|
+
*
|
|
19
|
+
* A resent request that fails is intercepted like any other, with
|
|
20
|
+
* `error.resent` telling how many times the request has been resent so far
|
|
21
|
+
* (0 for the original failure), so the callback can give up at some count.
|
|
22
|
+
* As a safety net, a request resent 10 times fails for good, its error
|
|
23
|
+
* thrown without reaching the callback.
|
|
24
|
+
*
|
|
25
|
+
* `hold()` holds the requests issued from then on (resent ones included)
|
|
26
|
+
* until the unhold function it returns is called. Requests already in flight
|
|
27
|
+
* are not affected, so several errors may reach the callback before a hold
|
|
28
|
+
* takes effect: managing them in one go (e.g. sharing a single token renewal)
|
|
29
|
+
* is up to the application.
|
|
30
|
+
*/
|
|
31
|
+
export default class ApiRecoveryPlugin {
|
|
32
|
+
|
|
33
|
+
static get id() {
|
|
34
|
+
return PLUGIN_ID;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
this._recoveries = new WeakMap();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
install(MisoClient, context) {
|
|
42
|
+
context.addApiMiddleware(this._middleware.bind(this));
|
|
43
|
+
MisoClient.on('create', this._injectClient.bind(this));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_injectClient(client) {
|
|
47
|
+
const recovery = new ApiRecovery();
|
|
48
|
+
this._recoveries.set(client, recovery);
|
|
49
|
+
delegateProperties(client.api, recovery, ['onError']);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async _middleware({ client, ...request }, next) {
|
|
53
|
+
const recovery = this._recoveries.get(client);
|
|
54
|
+
return recovery ? recovery.run(request, next) : next();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class ApiRecovery {
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
this._callback = undefined;
|
|
63
|
+
this._holds = new Set();
|
|
64
|
+
this._gate = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get onError() {
|
|
68
|
+
return this._callback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// a single callback: setting it replaces the current one, and setting it
|
|
72
|
+
// to undefined removes it
|
|
73
|
+
set onError(callback) {
|
|
74
|
+
if (callback != undefined && typeof callback !== 'function') {
|
|
75
|
+
throw new Error(`Expect onError to be a function: ${callback}`);
|
|
76
|
+
}
|
|
77
|
+
this._callback = callback || undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async run(request, next, resent = 0) {
|
|
81
|
+
try {
|
|
82
|
+
return await this._send(next);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const callback = this._callback;
|
|
85
|
+
if (!callback || resent >= MAX_RESENT) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (error && typeof error === 'object') {
|
|
89
|
+
error.resent = resent;
|
|
90
|
+
}
|
|
91
|
+
return callback(error, Object.freeze({
|
|
92
|
+
request,
|
|
93
|
+
hold: () => this._hold(),
|
|
94
|
+
resend: () => this.run(request, next, resent + 1),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _send(next) {
|
|
100
|
+
// loop: a new hold may be placed by the time the gate opens
|
|
101
|
+
while (this._gate) {
|
|
102
|
+
await this._gate.promise;
|
|
103
|
+
}
|
|
104
|
+
return next();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_hold() {
|
|
108
|
+
const hold = {};
|
|
109
|
+
this._holds.add(hold);
|
|
110
|
+
if (!this._gate) {
|
|
111
|
+
this._gate = new Resolution();
|
|
112
|
+
}
|
|
113
|
+
// holds overlap: the gate opens when the last one is released
|
|
114
|
+
return () => {
|
|
115
|
+
if (!this._holds.delete(hold) || this._holds.size) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const gate = this._gate;
|
|
119
|
+
this._gate = undefined;
|
|
120
|
+
gate.resolve();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
}
|
package/src/plugin/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { default as AutoEventsPlugin } from './auto-events.js';
|
|
|
4
4
|
export { default as InteractionsPlugin } from './interactions.js';
|
|
5
5
|
export { default as NativeFetchPlugin } from './native-fetch.js';
|
|
6
6
|
export { default as ApiPatchPlugin } from './api-patch.js';
|
|
7
|
+
export { default as ApiRecoveryPlugin } from './api-recovery.js';
|
|
7
8
|
export { default as HeaderApiKeyPlugin } from './header-api-key.js';
|
|
8
9
|
export { default as AnalyticsPlugin } from './analytics.js';
|
|
@@ -111,6 +111,13 @@ export default class PluginRoot extends Registry {
|
|
|
111
111
|
this._root._urlPasses.push(pass);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
addApiMiddleware(middleware) {
|
|
115
|
+
if (typeof middleware !== 'function') {
|
|
116
|
+
throw new Error(`Expect parameter to be a function: ${middleware}`);
|
|
117
|
+
}
|
|
118
|
+
this._root._apiMiddlewares.push(middleware);
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
setCustomFetch(fetch) {
|
|
115
122
|
if (typeof fetch !== 'function') {
|
|
116
123
|
throw new Error(`Expect parameter to be a function: ${fetch}`);
|
|
@@ -276,6 +283,7 @@ class PluginContext {
|
|
|
276
283
|
'addPayloadPass',
|
|
277
284
|
'addHeadersPass',
|
|
278
285
|
'addUrlPass',
|
|
286
|
+
'addApiMiddleware',
|
|
279
287
|
'setCustomFetch',
|
|
280
288
|
'setCustomSendBeacon',
|
|
281
289
|
'setCustomApiIterator',
|
package/src/root.js
CHANGED
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '1.14.0-beta.
|
|
1
|
+
export default '1.14.0-beta.4';
|