@jacebenson/jsn 4.3.10 → 4.3.11-dev.69

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/src/sdk.js CHANGED
@@ -50,6 +50,7 @@ export class SDKClient {
50
50
  this.timeout = opts.timeout || DEFAULT_TIMEOUT;
51
51
  this.domain = opts.domain || '';
52
52
  this.oauthCookies = null;
53
+ this._sessionPromise = null;
53
54
  this.sessionUserToken = '';
54
55
  }
55
56
 
@@ -94,74 +95,63 @@ export class SDKClient {
94
95
  }
95
96
  }
96
97
 
97
- async request(endpoint, opts = {}) {
98
- const { timeout = this.timeout, readOnly = false, touchLastSeen, ...requestOptions } = opts;
99
- if (readOnly && (requestOptions.method || 'GET').toUpperCase() !== 'GET') throw new Error('Read-only SDK requests require GET');
98
+ // Keep the timeout alive through body consumption for text/JSON requests.
99
+ // Raw-response callers retain their existing timeout boundary at headers.
100
+ async _withAuthenticatedRequest(endpoint, opts, {
101
+ timeout = this.timeout, prepare, skipOAuthSession = false, normalizeErrors = false,
102
+ }, consume = (response) => response) {
100
103
  const controller = new AbortController();
101
104
  const timer = setTimeout(() => controller.abort(), timeout);
102
-
103
105
  try {
104
- const req = new Request(endpoint, {
105
- ...requestOptions,
106
- signal: controller.signal,
107
- });
108
- req.headers.set('Accept', 'application/json');
109
- if (requestOptions.body && typeof requestOptions.body === 'string') {
110
- req.headers.set('Content-Type', 'application/json');
111
- }
112
- if (this.domain) {
113
- req.headers.set('X-Now-Domain', this.domain);
106
+ const req = new Request(endpoint, { ...opts, signal: controller.signal });
107
+ if (prepare) prepare(req);
108
+ await this._setAuth(req, { skipOAuthSession });
109
+ return await consume(await fetch(req));
110
+ } catch (err) {
111
+ if (normalizeErrors) {
112
+ if (err.name === 'AbortError') throw errNetwork(new Error('Request timed out'));
113
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
114
+ throw errNetwork(err);
115
+ }
114
116
  }
115
- // readOnly guards the data request, not standard authentication bootstrap.
116
- // OAuth refresh and session/GCK negotiation are not instance-record writes.
117
- await this._setAuth(req);
117
+ throw err;
118
+ } finally {
119
+ clearTimeout(timer);
120
+ }
121
+ }
118
122
 
119
- const resp = await fetch(req);
123
+ async request(endpoint, opts = {}) {
124
+ const { timeout = this.timeout, readOnly = false, touchLastSeen, ...requestOptions } = opts;
125
+ if (readOnly && (requestOptions.method || 'GET').toUpperCase() !== 'GET') throw new Error('Read-only SDK requests require GET');
126
+ return this._withAuthenticatedRequest(endpoint, requestOptions, {
127
+ timeout,
128
+ normalizeErrors: true,
129
+ prepare: (req) => {
130
+ req.headers.set('Accept', 'application/json');
131
+ if (requestOptions.body && typeof requestOptions.body === 'string') {
132
+ req.headers.set('Content-Type', 'application/json');
133
+ }
134
+ if (this.domain) req.headers.set('X-Now-Domain', this.domain);
135
+ },
136
+ }, async (resp) => {
120
137
  const body = await resp.text();
138
+ if (!resp.ok) throw errAPI(resp.status, body || resp.statusText);
139
+ if (resp.status === 204 || body === '') return null;
121
140
 
122
- if (!resp.ok) {
123
- throw errAPI(resp.status, body || resp.statusText);
124
- }
125
-
126
- if (resp.status === 204 || body === '') {
127
- return null;
128
- }
129
-
130
- // Stamp last_seen on successful API call
141
+ // Preserve read-only credential-write suppression and the existing empty-body behavior.
131
142
  try {
132
- // readOnly suppresses the credential last-seen write unconditionally: a
133
- // caller passing touchLastSeen cannot override it (GLOSSARY contract).
134
143
  const shouldTouch = readOnly ? false : (touchLastSeen ?? true);
135
144
  if (shouldTouch && this.authProvider && typeof this.authProvider.touchLastSeen === 'function') {
136
145
  this.authProvider.touchLastSeen(this.baseURL);
137
146
  }
138
147
  } catch { /* non-fatal */ }
139
-
140
148
  return JSON.parse(body);
141
- } catch (err) {
142
- if (err.name === 'AbortError') {
143
- throw errNetwork(new Error('Request timed out'));
144
- }
145
- if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
146
- throw errNetwork(err);
147
- }
148
- throw err;
149
- } finally {
150
- clearTimeout(timer);
151
- }
149
+ });
152
150
  }
153
151
 
154
- // _fetchWithAuth makes an authenticated fetch and returns the Response object.
152
+ // Authentication bootstrap bypasses session warming to avoid recursion.
155
153
  async _fetchWithAuth(endpoint, opts = {}) {
156
- const controller = new AbortController();
157
- const timer = setTimeout(() => controller.abort(), this.timeout);
158
- try {
159
- const req = new Request(endpoint, { ...opts, signal: controller.signal });
160
- await this._setAuth(req, { skipOAuthSession: true });
161
- return await fetch(req);
162
- } finally {
163
- clearTimeout(timer);
164
- }
154
+ return this._withAuthenticatedRequest(endpoint, opts, { skipOAuthSession: true });
165
155
  }
166
156
 
167
157
  // Public raw-response seam for callers that must inspect non-2xx bodies.
@@ -170,38 +160,18 @@ export class SDKClient {
170
160
  }
171
161
 
172
162
  async rawRequest(endpoint, opts = {}) {
173
- const controller = new AbortController();
174
- const timer = setTimeout(() => controller.abort(), this.timeout);
175
-
176
- try {
177
- const req = new Request(endpoint, {
178
- ...opts,
179
- signal: controller.signal,
180
- });
181
- if (opts.body && typeof opts.body === 'string') {
182
- req.headers.set('Content-Type', opts.headers?.['Content-Type'] || 'application/x-www-form-urlencoded');
183
- }
184
- await this._setAuth(req);
185
-
186
- const resp = await fetch(req);
163
+ return this._withAuthenticatedRequest(endpoint, opts, {
164
+ normalizeErrors: true,
165
+ prepare: (req) => {
166
+ if (opts.body && typeof opts.body === 'string') {
167
+ req.headers.set('Content-Type', opts.headers?.['Content-Type'] || 'application/x-www-form-urlencoded');
168
+ }
169
+ },
170
+ }, async (resp) => {
187
171
  const body = await resp.text();
188
-
189
- if (!resp.ok) {
190
- throw errAPI(resp.status, body || resp.statusText);
191
- }
192
-
172
+ if (!resp.ok) throw errAPI(resp.status, body || resp.statusText);
193
173
  return body;
194
- } catch (err) {
195
- if (err.name === 'AbortError') {
196
- throw errNetwork(new Error('Request timed out'));
197
- }
198
- if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
199
- throw errNetwork(err);
200
- }
201
- throw err;
202
- } finally {
203
- clearTimeout(timer);
204
- }
174
+ });
205
175
  }
206
176
 
207
177
  async getSkippedUpdateBaseline(sysID) {
@@ -791,18 +761,26 @@ export class SDKClient {
791
761
 
792
762
  const result = { _record: records[0] };
793
763
 
794
- for (const rel of related) {
795
- try {
796
- const relParams = new URLSearchParams();
797
- relParams.set('sysparm_display_value', 'all');
798
- relParams.set('sysparm_fields', (rel.fields || []).join(','));
799
- relParams.set('sysparm_query', `${rel.queryField}=${rel.queryValue}`);
800
- relParams.set('sysparm_limit', '100');
801
- result[rel.displayAs] = await this.list(rel.table, relParams);
802
- } catch {
803
- result[rel.displayAs] = [];
764
+ // Four workers bound instance load while overlapping independent reads.
765
+ let next = 0;
766
+ const values = new Array(related.length);
767
+ await Promise.all(Array.from({ length: Math.min(4, related.length) }, async () => {
768
+ while (next < related.length) {
769
+ const index = next++;
770
+ const rel = related[index];
771
+ try {
772
+ const relParams = new URLSearchParams();
773
+ relParams.set('sysparm_display_value', 'all');
774
+ relParams.set('sysparm_fields', (rel.fields || []).join(','));
775
+ relParams.set('sysparm_query', `${rel.queryField}=${rel.queryValue}`);
776
+ relParams.set('sysparm_limit', '100');
777
+ values[index] = await this.list(rel.table, relParams);
778
+ } catch {
779
+ values[index] = [];
780
+ }
804
781
  }
805
- }
782
+ }));
783
+ related.forEach((rel, index) => { result[rel.displayAs] = values[index]; });
806
784
 
807
785
  return result;
808
786
  }
@@ -859,6 +837,15 @@ export class SDKClient {
859
837
 
860
838
  async _warmSession() {
861
839
  if (this.oauthCookies) return this.oauthCookies;
840
+ if (!this._sessionPromise) {
841
+ this._sessionPromise = this._initializeSession().finally(() => {
842
+ this._sessionPromise = null;
843
+ });
844
+ }
845
+ return this._sessionPromise;
846
+ }
847
+
848
+ async _initializeSession() {
862
849
  try {
863
850
  const creds = await this.authProvider.getCredentials();
864
851
  if (creds.auth_method === 'oauth') {