@miso.ai/client-sdk-core 1.14.0-beta.1 → 1.14.0-beta.3

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.
@@ -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._run(NAME.ANSWERS, payload, options);
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));
@@ -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({
@@ -196,4 +229,11 @@ class ApiHelpers {
196
229
  return headers;
197
230
  }
198
231
  }
199
- exports.default = ApiHelpers;
232
+
233
+ // no status: a network failure, a timeout, or an unparsable body — all
234
+ // transport-level; 5xx: the server's own failure. A 4xx or an
235
+ // application-level error (2xx with `errors`) is deterministic
236
+ exports.default = ApiHelpers;
237
+ function isRetryableFetchError(error) {
238
+ return error.status === undefined || error.status >= 500;
239
+ }
@@ -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', undefined, Object.assign({}, options, {
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}`, undefined, Object.assign({}, options, {
22
+ async getThread(threadId, payload, options) {
23
+ return this._run(`threads/${threadId}`, payload, Object.assign({}, options, {
24
24
  method: 'GET'
25
25
  }));
26
26
  }
@@ -22,13 +22,7 @@ class IterableApiStub {
22
22
  this._ac.abort(reason);
23
23
  }
24
24
  [Symbol.asyncIterator]() {
25
- const fetch = async ({
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 (isUpdated(prevResponse, response)) {
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 {
@@ -4,4 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _default = exports.default = '1.14.0-beta.1';
7
+ var _default = exports.default = '1.14.0-beta.3';
package/package.json CHANGED
@@ -30,7 +30,7 @@
30
30
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
31
31
  },
32
32
  "dependencies": {
33
- "@miso.ai/commons": "1.14.0-beta.1"
33
+ "@miso.ai/commons": "1.14.0-beta.3"
34
34
  },
35
- "version": "1.14.0-beta.1"
35
+ "version": "1.14.0-beta.3"
36
36
  }
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._run(NAME.ANSWERS, payload, options);
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 = {}) {
@@ -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 { apiKey, request = {} } = this._client.options;
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
 
@@ -143,3 +165,10 @@ export default class ApiHelpers {
143
165
  }
144
166
 
145
167
  }
168
+
169
+ // no status: a network failure, a timeout, or an unparsable body — all
170
+ // transport-level; 5xx: the server's own failure. A 4xx or an
171
+ // application-level error (2xx with `errors`) is deterministic
172
+ function isRetryableFetchError(error) {
173
+ return error.status === undefined || error.status >= 500;
174
+ }
@@ -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', undefined, { ...options, method: 'GET' });
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}`, undefined, { ...options, method: 'GET' });
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) {
@@ -20,11 +20,7 @@ export class IterableApiStub {
20
20
  }
21
21
 
22
22
  [Symbol.asyncIterator]() {
23
- const fetch = async ({ signal } = {}) => {
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 (isUpdated(prevResponse, response)) {
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/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.14.0-beta.1';
1
+ export default '1.14.0-beta.3';