@dataworks-technology/data 0.1.4 → 0.1.6

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/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Public types for @dataworks/data.
2
+ * Public types for @dataworks-technology/data.
3
3
  *
4
4
  * @module @dataworks-technology/data
5
5
  */
@@ -9,6 +9,8 @@ interface DataClientConfig {
9
9
  cognitoEndpoint: string;
10
10
  /** Cognito app client ID */
11
11
  clientId: string;
12
+ /** Optional Cognito app client secret (if configured on the app client) */
13
+ clientSecret?: string;
12
14
  /** Data Engine ingest URL (API Gateway endpoint) */
13
15
  ingestUrl: string;
14
16
  /** Data Engine error reporting URL (API Gateway endpoint) */
@@ -70,6 +72,8 @@ interface ErrorReport {
70
72
  }
71
73
  /** Callback for subscription events. */
72
74
  type SubscriptionHandler = (event: unknown) => void;
75
+ /** Callback for subscription errors (WebSocket errors, AppSync errors, reconnect failures). */
76
+ type ErrorHandler = (error: Error) => void;
73
77
  /** Active subscription that can be closed. */
74
78
  interface Subscription {
75
79
  /** Close the subscription and disconnect. */
@@ -79,6 +83,8 @@ interface Subscription {
79
83
  declare class DataClient {
80
84
  private readonly config;
81
85
  private credentials;
86
+ private lastLoginUsername;
87
+ private refreshInFlight;
82
88
  constructor(config: DataClientConfig);
83
89
  /**
84
90
  * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
@@ -87,6 +93,7 @@ declare class DataClient {
87
93
  * @param username - Cognito username
88
94
  * @param password - Cognito password
89
95
  * @returns Login result containing tokens and tenant
96
+ * @see https://data-sdk-docs.dataworks.live/authentication
90
97
  */
91
98
  login(username: string, password: string): Promise<LoginResult>;
92
99
  /**
@@ -97,6 +104,7 @@ declare class DataClient {
97
104
  * @param eventId - Event identifier
98
105
  * @param datasetDatasourceId - Dataset-datasource identifier
99
106
  * @throws Error if not authenticated or if the request fails
107
+ * @see https://data-sdk-docs.dataworks.live/ingesting-data
100
108
  */
101
109
  ingest(metrics: Metric[], eventId: string, datasetDatasourceId: string): Promise<void>;
102
110
  /**
@@ -104,20 +112,24 @@ declare class DataClient {
104
112
  *
105
113
  * @param error - Error report details
106
114
  * @throws Error if not authenticated or if the request fails
115
+ * @see https://data-sdk-docs.dataworks.live/error-reporting
107
116
  */
108
117
  reportError(error: ErrorReport): Promise<void>;
109
118
  /**
110
119
  * Subscribe to a real-time data channel on the AppSync Events API.
111
120
  * Uses the Cognito JWT for authentication.
112
121
  *
113
- * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
122
+ * Channel format: dataworks/{datasetDatasourceId}/{eventId}/{metric}
123
+ * Use "*" for metric to receive all metrics for a dataset-datasource/event pair.
124
+ * Examples: "dataworks/1/1/heartrate", "dataworks/1/1/*".
114
125
  *
115
126
  * @param channel - Channel path to subscribe to
116
127
  * @param onEvent - Callback invoked for each received event
117
128
  * @returns Subscription handle with a close() method
118
129
  * @throws Error if not authenticated
130
+ * @see https://data-sdk-docs.dataworks.live/real-time-subscriptions
119
131
  */
120
- subscribe(channel: string, onEvent: SubscriptionHandler): Subscription;
132
+ subscribe(channel: string, onEvent: SubscriptionHandler, onError?: ErrorHandler): Subscription;
121
133
  /**
122
134
  * Check whether a metric is valid before ingesting.
123
135
  * Returns null if valid, or a human-readable reason string if invalid.
@@ -134,6 +146,8 @@ declare class DataClient {
134
146
  get tenant(): string | null;
135
147
  /** @internal Throws if not authenticated. */
136
148
  private requireAuth;
149
+ private fetchWithAutoRefresh;
150
+ private refreshSession;
137
151
  }
138
152
 
139
153
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Public types for @dataworks/data.
2
+ * Public types for @dataworks-technology/data.
3
3
  *
4
4
  * @module @dataworks-technology/data
5
5
  */
@@ -9,6 +9,8 @@ interface DataClientConfig {
9
9
  cognitoEndpoint: string;
10
10
  /** Cognito app client ID */
11
11
  clientId: string;
12
+ /** Optional Cognito app client secret (if configured on the app client) */
13
+ clientSecret?: string;
12
14
  /** Data Engine ingest URL (API Gateway endpoint) */
13
15
  ingestUrl: string;
14
16
  /** Data Engine error reporting URL (API Gateway endpoint) */
@@ -70,6 +72,8 @@ interface ErrorReport {
70
72
  }
71
73
  /** Callback for subscription events. */
72
74
  type SubscriptionHandler = (event: unknown) => void;
75
+ /** Callback for subscription errors (WebSocket errors, AppSync errors, reconnect failures). */
76
+ type ErrorHandler = (error: Error) => void;
73
77
  /** Active subscription that can be closed. */
74
78
  interface Subscription {
75
79
  /** Close the subscription and disconnect. */
@@ -79,6 +83,8 @@ interface Subscription {
79
83
  declare class DataClient {
80
84
  private readonly config;
81
85
  private credentials;
86
+ private lastLoginUsername;
87
+ private refreshInFlight;
82
88
  constructor(config: DataClientConfig);
83
89
  /**
84
90
  * Authenticate with the Dataworks platform using Cognito USER_PASSWORD_AUTH.
@@ -87,6 +93,7 @@ declare class DataClient {
87
93
  * @param username - Cognito username
88
94
  * @param password - Cognito password
89
95
  * @returns Login result containing tokens and tenant
96
+ * @see https://data-sdk-docs.dataworks.live/authentication
90
97
  */
91
98
  login(username: string, password: string): Promise<LoginResult>;
92
99
  /**
@@ -97,6 +104,7 @@ declare class DataClient {
97
104
  * @param eventId - Event identifier
98
105
  * @param datasetDatasourceId - Dataset-datasource identifier
99
106
  * @throws Error if not authenticated or if the request fails
107
+ * @see https://data-sdk-docs.dataworks.live/ingesting-data
100
108
  */
101
109
  ingest(metrics: Metric[], eventId: string, datasetDatasourceId: string): Promise<void>;
102
110
  /**
@@ -104,20 +112,24 @@ declare class DataClient {
104
112
  *
105
113
  * @param error - Error report details
106
114
  * @throws Error if not authenticated or if the request fails
115
+ * @see https://data-sdk-docs.dataworks.live/error-reporting
107
116
  */
108
117
  reportError(error: ErrorReport): Promise<void>;
109
118
  /**
110
119
  * Subscribe to a real-time data channel on the AppSync Events API.
111
120
  * Uses the Cognito JWT for authentication.
112
121
  *
113
- * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
122
+ * Channel format: dataworks/{datasetDatasourceId}/{eventId}/{metric}
123
+ * Use "*" for metric to receive all metrics for a dataset-datasource/event pair.
124
+ * Examples: "dataworks/1/1/heartrate", "dataworks/1/1/*".
114
125
  *
115
126
  * @param channel - Channel path to subscribe to
116
127
  * @param onEvent - Callback invoked for each received event
117
128
  * @returns Subscription handle with a close() method
118
129
  * @throws Error if not authenticated
130
+ * @see https://data-sdk-docs.dataworks.live/real-time-subscriptions
119
131
  */
120
- subscribe(channel: string, onEvent: SubscriptionHandler): Subscription;
132
+ subscribe(channel: string, onEvent: SubscriptionHandler, onError?: ErrorHandler): Subscription;
121
133
  /**
122
134
  * Check whether a metric is valid before ingesting.
123
135
  * Returns null if valid, or a human-readable reason string if invalid.
@@ -134,6 +146,8 @@ declare class DataClient {
134
146
  get tenant(): string | null;
135
147
  /** @internal Throws if not authenticated. */
136
148
  private requireAuth;
149
+ private fetchWithAutoRefresh;
150
+ private refreshSession;
137
151
  }
138
152
 
139
153
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,46 @@
1
- // ../../node_modules/@dataworks/sdk/dist/client/validation.js
1
+ // node_modules/@dataworks/sdk/dist/client/subscription-manager.js
2
+ function createAutoRefreshingSubscription(options) {
3
+ const maxReconnectAttempts = options.maxReconnectAttempts ?? 1;
4
+ let reconnectAttempts = 0;
5
+ let reconnectInFlight = false;
6
+ let closedByUser = false;
7
+ let activeConnection = null;
8
+ const hooks = {
9
+ onUnexpectedClose: () => {
10
+ if (closedByUser || reconnectInFlight) {
11
+ return;
12
+ }
13
+ if (reconnectAttempts >= maxReconnectAttempts) {
14
+ return;
15
+ }
16
+ reconnectInFlight = true;
17
+ reconnectAttempts += 1;
18
+ void (async () => {
19
+ try {
20
+ await options.refreshAuth();
21
+ if (closedByUser) {
22
+ return;
23
+ }
24
+ activeConnection = options.connect(hooks);
25
+ } catch {
26
+ options.onReconnectError?.(new Error("Subscription reconnect failed. Please retry or reauthenticate."));
27
+ } finally {
28
+ reconnectInFlight = false;
29
+ }
30
+ })();
31
+ }
32
+ };
33
+ activeConnection = options.connect(hooks);
34
+ return {
35
+ close() {
36
+ closedByUser = true;
37
+ activeConnection?.close();
38
+ activeConnection = null;
39
+ }
40
+ };
41
+ }
42
+
43
+ // node_modules/@dataworks/sdk/dist/client/validation.js
2
44
  function validateMetric(m) {
3
45
  if (typeof m.metric !== "string" || m.metric.trim() === "")
4
46
  return "metric must be a non-empty string";
@@ -17,28 +59,59 @@ function validateMetric(m) {
17
59
  return null;
18
60
  }
19
61
  function filterValidMetrics(metrics, warn) {
62
+ const { valid, dropped } = getMetricValidationResult(metrics);
63
+ for (const item of dropped) {
64
+ let valueStr;
65
+ try {
66
+ valueStr = JSON.stringify(item.metric.value);
67
+ } catch {
68
+ valueStr = String(item.metric.value);
69
+ }
70
+ warn(`Dropping invalid metric [${String(item.metric.metric)}=${valueStr}]: ${item.reason}`);
71
+ }
72
+ return valid;
73
+ }
74
+ function getMetricValidationResult(metrics) {
20
75
  const valid = [];
21
- for (const m of metrics) {
76
+ const dropped = [];
77
+ for (const [index, m] of metrics.entries()) {
22
78
  const reason = validateMetric(m);
23
79
  if (reason) {
24
- warn(`Dropping invalid metric [${String(m.metric)}=${JSON.stringify(m.value)}]: ${reason}`);
80
+ dropped.push({
81
+ index,
82
+ reason,
83
+ metric: m
84
+ });
25
85
  } else {
26
86
  valid.push(m);
27
87
  }
28
88
  }
29
- return valid;
89
+ return { valid, dropped };
30
90
  }
31
91
 
32
- // ../../node_modules/@dataworks/sdk/dist/client/auth.js
92
+ // node_modules/@dataworks/sdk/dist/client/base64url.js
93
+ var toBase64Url = (input) => {
94
+ if (typeof Buffer !== "undefined") {
95
+ return Buffer.from(input).toString("base64url");
96
+ }
97
+ return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
98
+ };
99
+ var fromBase64Url = (b64url) => {
100
+ if (typeof Buffer !== "undefined") {
101
+ return Buffer.from(b64url, "base64url").toString("utf-8");
102
+ }
103
+ const padded = b64url.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((-b64url.length % 4 + 4) % 4);
104
+ return atob(padded);
105
+ };
106
+
107
+ // node_modules/@dataworks/sdk/dist/client/auth.js
33
108
  async function loginWithCredentials(username, password, config) {
34
109
  const authParams = {
35
110
  USERNAME: username,
36
111
  PASSWORD: password
37
112
  };
38
113
  if (config.clientSecret) {
39
- const crypto2 = await import("crypto");
40
- const hash = crypto2.createHmac("sha256", config.clientSecret).update(username + config.clientId).digest("base64");
41
- authParams.SECRET_HASH = hash;
114
+ authParams.SECRET_HASH = await computeSecretHashAsync(username, config.clientId, config.clientSecret);
42
115
  }
43
116
  const resp = await fetch(config.cognitoEndpoint, {
44
117
  method: "POST",
@@ -69,15 +142,60 @@ async function loginWithCredentials(username, password, config) {
69
142
  tenant
70
143
  };
71
144
  }
145
+ async function refreshWithToken(refreshToken, config) {
146
+ const authParams = {
147
+ REFRESH_TOKEN: refreshToken
148
+ };
149
+ if (config.clientSecret) {
150
+ if (!config.username) {
151
+ throw new Error("refreshWithToken: username is required when clientSecret is configured");
152
+ }
153
+ authParams.SECRET_HASH = await computeSecretHashAsync(config.username, config.clientId, config.clientSecret);
154
+ }
155
+ const resp = await fetch(config.cognitoEndpoint, {
156
+ method: "POST",
157
+ headers: {
158
+ "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
159
+ "Content-Type": "application/x-amz-json-1.1"
160
+ },
161
+ body: JSON.stringify({
162
+ AuthFlow: "REFRESH_TOKEN_AUTH",
163
+ ClientId: config.clientId,
164
+ AuthParameters: authParams
165
+ })
166
+ });
167
+ if (!resp.ok) {
168
+ const body = await resp.text().catch(() => "");
169
+ throw new Error(`refreshWithToken: Cognito refresh failed: ${resp.status} \u2014 ${body}`);
170
+ }
171
+ const data = await resp.json();
172
+ const result = data.AuthenticationResult;
173
+ if (!result?.AccessToken) {
174
+ throw new Error("refreshWithToken: response missing AccessToken");
175
+ }
176
+ const idToken = result.IdToken ?? config.previousIdToken ?? "";
177
+ const extractedTenant = idToken ? extractTenantFromJwt(idToken) : "";
178
+ const tenant = extractedTenant || config.previousTenant || "";
179
+ return {
180
+ accessToken: result.AccessToken,
181
+ idToken,
182
+ refreshToken: result.RefreshToken ?? refreshToken,
183
+ tenant
184
+ };
185
+ }
72
186
  function extractTenantFromJwt(idToken) {
73
187
  try {
74
188
  const payload = idToken.split(".")[1];
75
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
76
- return decoded["custom:tenant"] ?? decoded.tenant ?? decoded["cognito:groups"]?.[0] ?? "";
189
+ const decoded = JSON.parse(fromBase64Url(payload));
190
+ return decoded["custom:tenants"] ?? decoded["custom:tenant"] ?? decoded.tenant ?? decoded["cognito:groups"]?.[0] ?? "";
77
191
  } catch {
78
192
  return "";
79
193
  }
80
194
  }
195
+ async function computeSecretHashAsync(username, clientId, clientSecret) {
196
+ const crypto2 = await import("crypto");
197
+ return crypto2.createHmac("sha256", clientSecret).update(username + clientId).digest("base64");
198
+ }
81
199
 
82
200
  // src/validation.ts
83
201
  var validateMetric2 = validateMetric;
@@ -87,6 +205,8 @@ var filterValidMetrics2 = filterValidMetrics;
87
205
  var DataClient = class {
88
206
  constructor(config) {
89
207
  this.credentials = null;
208
+ this.lastLoginUsername = null;
209
+ this.refreshInFlight = null;
90
210
  this.config = config;
91
211
  }
92
212
  /**
@@ -96,11 +216,14 @@ var DataClient = class {
96
216
  * @param username - Cognito username
97
217
  * @param password - Cognito password
98
218
  * @returns Login result containing tokens and tenant
219
+ * @see https://data-sdk-docs.dataworks.live/authentication
99
220
  */
100
221
  async login(username, password) {
222
+ this.lastLoginUsername = username;
101
223
  this.credentials = await loginWithCredentials(username, password, {
102
224
  cognitoEndpoint: this.config.cognitoEndpoint,
103
- clientId: this.config.clientId
225
+ clientId: this.config.clientId,
226
+ clientSecret: this.config.clientSecret
104
227
  });
105
228
  return this.credentials;
106
229
  }
@@ -112,6 +235,7 @@ var DataClient = class {
112
235
  * @param eventId - Event identifier
113
236
  * @param datasetDatasourceId - Dataset-datasource identifier
114
237
  * @throws Error if not authenticated or if the request fails
238
+ * @see https://data-sdk-docs.dataworks.live/ingesting-data
115
239
  */
116
240
  async ingest(metrics, eventId, datasetDatasourceId) {
117
241
  this.requireAuth();
@@ -125,14 +249,16 @@ var DataClient = class {
125
249
  datasetDatasourceId,
126
250
  metrics: valid
127
251
  };
128
- const resp = await fetch(this.config.ingestUrl, {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- Authorization: `Bearer ${this.credentials.accessToken}`
133
- },
134
- body: JSON.stringify(payload)
135
- });
252
+ const resp = await this.fetchWithAutoRefresh(
253
+ (accessToken) => fetch(this.config.ingestUrl, {
254
+ method: "POST",
255
+ headers: {
256
+ "Content-Type": "application/json",
257
+ Authorization: `Bearer ${accessToken}`
258
+ },
259
+ body: JSON.stringify(payload)
260
+ })
261
+ );
136
262
  if (!resp.ok) {
137
263
  const body = await resp.text().catch(() => "");
138
264
  throw new Error(
@@ -145,23 +271,26 @@ var DataClient = class {
145
271
  *
146
272
  * @param error - Error report details
147
273
  * @throws Error if not authenticated or if the request fails
274
+ * @see https://data-sdk-docs.dataworks.live/error-reporting
148
275
  */
149
276
  async reportError(error) {
150
277
  this.requireAuth();
151
- const resp = await fetch(this.config.errorUrl, {
152
- method: "POST",
153
- headers: {
154
- "Content-Type": "application/json",
155
- Authorization: `Bearer ${this.credentials.accessToken}`
156
- },
157
- body: JSON.stringify({
158
- client_id: String(error.clientId),
159
- dataset_datasource_id: error.datasetDatasourceId,
160
- athlete_id: error.athleteId,
161
- error_title: error.errorTitle,
162
- error_description: error.errorDescription
278
+ const resp = await this.fetchWithAutoRefresh(
279
+ (accessToken) => fetch(this.config.errorUrl, {
280
+ method: "POST",
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ Authorization: `Bearer ${accessToken}`
284
+ },
285
+ body: JSON.stringify({
286
+ client_id: String(error.clientId),
287
+ dataset_datasource_id: error.datasetDatasourceId,
288
+ athlete_id: error.athleteId,
289
+ error_title: error.errorTitle,
290
+ error_description: error.errorDescription
291
+ })
163
292
  })
164
- });
293
+ );
165
294
  if (!resp.ok) {
166
295
  const body = await resp.text().catch(() => "");
167
296
  throw new Error(
@@ -173,56 +302,107 @@ var DataClient = class {
173
302
  * Subscribe to a real-time data channel on the AppSync Events API.
174
303
  * Uses the Cognito JWT for authentication.
175
304
  *
176
- * The channel name maps to the AppSync Events API namespace (e.g. "dataworks/metrics").
305
+ * Channel format: dataworks/{datasetDatasourceId}/{eventId}/{metric}
306
+ * Use "*" for metric to receive all metrics for a dataset-datasource/event pair.
307
+ * Examples: "dataworks/1/1/heartrate", "dataworks/1/1/*".
177
308
  *
178
309
  * @param channel - Channel path to subscribe to
179
310
  * @param onEvent - Callback invoked for each received event
180
311
  * @returns Subscription handle with a close() method
181
312
  * @throws Error if not authenticated
313
+ * @see https://data-sdk-docs.dataworks.live/real-time-subscriptions
182
314
  */
183
- subscribe(channel, onEvent) {
315
+ subscribe(channel, onEvent, onError) {
184
316
  this.requireAuth();
185
- const url = new URL(this.config.realtimeUrl);
186
- url.pathname = "/event/realtime";
187
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
188
- const authPayload = JSON.stringify({
189
- Authorization: this.credentials.accessToken,
190
- host: new URL(this.config.realtimeUrl).host
191
- });
192
- const authHeader = btoa(authPayload).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
193
- const ws = new WebSocket(url.toString(), [
194
- "aws-appsync-event-ws",
195
- `header-${authHeader}`
196
- ]);
197
- ws.addEventListener("open", () => {
198
- ws.send(JSON.stringify({ type: "connection_init" }));
199
- });
200
- ws.addEventListener("message", (event) => {
201
- try {
202
- const msg = JSON.parse(String(event.data));
203
- if (msg.type === "connection_ack") {
204
- ws.send(
205
- JSON.stringify({
206
- type: "subscribe",
207
- id: crypto.randomUUID(),
208
- channel: `/${channel}`,
209
- authorization: {
210
- Authorization: this.credentials.accessToken,
211
- host: new URL(this.config.realtimeUrl).host
317
+ if (typeof WebSocket === "undefined") {
318
+ throw new Error(
319
+ "[@dataworks-technology/data] WebSocket is not available. Use Node >= 22, Bun, or a browser environment. For older Node versions, install a WebSocket polyfill (e.g. ws) and assign it to globalThis.WebSocket."
320
+ );
321
+ }
322
+ if (typeof crypto?.randomUUID !== "function") {
323
+ throw new Error(
324
+ "[@dataworks-technology/data] crypto.randomUUID() is not available. Use Node >= 19, Bun, or a browser with Crypto API support."
325
+ );
326
+ }
327
+ const channelPath = channel.startsWith("/") ? channel : `/${channel}`;
328
+ return createAutoRefreshingSubscription({
329
+ refreshAuth: async () => {
330
+ await this.refreshSession();
331
+ },
332
+ onReconnectError: (error) => {
333
+ onError?.(error);
334
+ },
335
+ connect: ({ onUnexpectedClose }) => {
336
+ const url = new URL(this.config.realtimeUrl);
337
+ url.pathname = "/event/realtime";
338
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
339
+ const host = new URL(this.config.realtimeUrl).host;
340
+ const authPayload = JSON.stringify({
341
+ Authorization: this.credentials.accessToken,
342
+ host
343
+ });
344
+ const authHeader = toBase64Url(authPayload);
345
+ const ws = new WebSocket(url.toString(), [
346
+ "aws-appsync-event-ws",
347
+ `header-${authHeader}`
348
+ ]);
349
+ let closedByUser = false;
350
+ let unexpectedCloseHandled = false;
351
+ const handleUnexpectedCloseOnce = () => {
352
+ if (unexpectedCloseHandled) return;
353
+ unexpectedCloseHandled = true;
354
+ onUnexpectedClose();
355
+ };
356
+ ws.addEventListener("open", () => {
357
+ ws.send(JSON.stringify({ type: "connection_init" }));
358
+ });
359
+ ws.addEventListener("error", () => {
360
+ onError?.(new Error("[@dataworks-technology/data] WebSocket connection error"));
361
+ });
362
+ ws.addEventListener("message", (event) => {
363
+ try {
364
+ const msg = JSON.parse(String(event.data));
365
+ if (msg.type === "connection_ack") {
366
+ ws.send(
367
+ JSON.stringify({
368
+ type: "subscribe",
369
+ id: crypto.randomUUID(),
370
+ channel: channelPath,
371
+ authorization: {
372
+ Authorization: this.credentials.accessToken,
373
+ host
374
+ }
375
+ })
376
+ );
377
+ } else if (msg.type === "data") {
378
+ onEvent(JSON.parse(msg.event));
379
+ } else if (msg.type === "error") {
380
+ const msgStr = JSON.stringify(msg);
381
+ if (msgStr.toLowerCase().includes("unauthor")) {
382
+ handleUnexpectedCloseOnce();
383
+ } else {
384
+ const errorMessage = msg.message ?? msg.errors?.[0]?.message ?? "AppSync subscription error";
385
+ onError?.(
386
+ new Error(`[@dataworks-technology/data] ${errorMessage}`)
387
+ );
212
388
  }
213
- })
214
- );
215
- } else if (msg.type === "data") {
216
- onEvent(JSON.parse(msg.event));
217
- }
218
- } catch {
389
+ }
390
+ } catch {
391
+ }
392
+ });
393
+ ws.addEventListener("close", () => {
394
+ if (!closedByUser) {
395
+ handleUnexpectedCloseOnce();
396
+ }
397
+ });
398
+ return {
399
+ close() {
400
+ closedByUser = true;
401
+ ws.close();
402
+ }
403
+ };
219
404
  }
220
405
  });
221
- return {
222
- close() {
223
- ws.close();
224
- }
225
- };
226
406
  }
227
407
  /** Returns true if the client has been authenticated via login(). */
228
408
  get isAuthenticated() {
@@ -240,6 +420,42 @@ var DataClient = class {
240
420
  );
241
421
  }
242
422
  }
423
+ async fetchWithAutoRefresh(makeRequest) {
424
+ this.requireAuth();
425
+ let response = await makeRequest(this.credentials.accessToken);
426
+ if (response.status !== 401) {
427
+ return response;
428
+ }
429
+ await this.refreshSession();
430
+ response = await makeRequest(this.credentials.accessToken);
431
+ return response;
432
+ }
433
+ async refreshSession() {
434
+ this.requireAuth();
435
+ if (!this.credentials.refreshToken) {
436
+ throw new Error(
437
+ "[@dataworks-technology/data] Session expired and no refresh token is available \u2014 call login() again"
438
+ );
439
+ }
440
+ if (!this.refreshInFlight) {
441
+ this.refreshInFlight = (async () => {
442
+ this.credentials = await refreshWithToken(
443
+ this.credentials.refreshToken,
444
+ {
445
+ cognitoEndpoint: this.config.cognitoEndpoint,
446
+ clientId: this.config.clientId,
447
+ clientSecret: this.config.clientSecret,
448
+ username: this.lastLoginUsername ?? void 0,
449
+ previousTenant: this.credentials.tenant,
450
+ previousIdToken: this.credentials.idToken
451
+ }
452
+ );
453
+ })().finally(() => {
454
+ this.refreshInFlight = null;
455
+ });
456
+ }
457
+ await this.refreshInFlight;
458
+ }
243
459
  };
244
460
  /**
245
461
  * Check whether a metric is valid before ingesting.