@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/README.md +93 -38
- package/dist/index.cjs +288 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +288 -72
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
76
|
+
const dropped = [];
|
|
77
|
+
for (const [index, m] of metrics.entries()) {
|
|
22
78
|
const reason = validateMetric(m);
|
|
23
79
|
if (reason) {
|
|
24
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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.
|