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

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.
@@ -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;
@@ -164,6 +164,27 @@ class ApiHelpers {
164
164
  } = this._client.options;
165
165
  return apiName === 'interactions' ? eventEndpoint : dataEndpoint;
166
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
+ }
167
188
  applyUrlPasses(component, {
168
189
  apiGroup,
169
190
  apiName,
@@ -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
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
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: {
@@ -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
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
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
+ }
@@ -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
@@ -23,6 +23,7 @@ class Root extends _commons.Component {
23
23
  this._payloadPasses = [];
24
24
  this._headersPasses = [];
25
25
  this._urlPasses = [];
26
+ this._apiMiddlewares = [];
26
27
  this._customFetch = undefined;
27
28
  this._customSendBeacon = undefined;
28
29
  this._hubUpdateCallbacks = [];
@@ -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.3';
7
+ var _default = exports.default = '1.14.0';
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.3"
33
+ "@miso.ai/commons": "1.14.0"
34
34
  },
35
- "version": "1.14.0-beta.3"
35
+ "version": "1.14.0"
36
36
  }
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;
@@ -128,6 +128,16 @@ export default class ApiHelpers {
128
128
  return apiName === 'interactions' ? eventEndpoint : dataEndpoint;
129
129
  }
130
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
+
131
141
  applyUrlPasses(component, { apiGroup, apiName, url }) {
132
142
  const client = this._client;
133
143
  for (const pass of this._root._urlPasses) {
@@ -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
+ }
@@ -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
@@ -15,6 +15,7 @@ class Root extends Component {
15
15
  this._payloadPasses = [];
16
16
  this._headersPasses = [];
17
17
  this._urlPasses = [];
18
+ this._apiMiddlewares = [];
18
19
  this._customFetch = undefined;
19
20
  this._customSendBeacon = undefined;
20
21
  this._hubUpdateCallbacks = [];
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.14.0-beta.3';
1
+ export default '1.14.0';