@rool-dev/sdk 0.12.4 → 2.0.0-dev.03df95a
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 +462 -758
- package/dist/agents.d.ts +138 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +563 -0
- package/dist/agents.js.map +1 -0
- package/dist/auth-base.d.ts +43 -88
- package/dist/auth-base.d.ts.map +1 -1
- package/dist/auth-base.js +178 -298
- package/dist/auth-base.js.map +1 -1
- package/dist/auth-browser.d.ts +7 -25
- package/dist/auth-browser.d.ts.map +1 -1
- package/dist/auth-browser.js +45 -82
- package/dist/auth-browser.js.map +1 -1
- package/dist/auth-native.d.ts +9 -56
- package/dist/auth-native.d.ts.map +1 -1
- package/dist/auth-native.js +88 -177
- package/dist/auth-native.js.map +1 -1
- package/dist/auth-node.d.ts +32 -32
- package/dist/auth-node.d.ts.map +1 -1
- package/dist/auth-node.js +339 -255
- package/dist/auth-node.js.map +1 -1
- package/dist/auth-url.d.ts +6 -0
- package/dist/auth-url.d.ts.map +1 -0
- package/dist/auth-url.js +30 -0
- package/dist/auth-url.js.map +1 -0
- package/dist/client.d.ts +37 -277
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +331 -618
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +40 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +175 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +174 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +846 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +16 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -19
- package/dist/index.js.map +1 -1
- package/dist/machine.d.ts +51 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +142 -0
- package/dist/machine.js.map +1 -0
- package/dist/problem.d.ts +11 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +20 -0
- package/dist/problem.js.map +1 -0
- package/dist/structured.d.ts +84 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/structured.js +405 -0
- package/dist/structured.js.map +1 -0
- package/dist/types.d.ts +131 -520
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -16
- package/dist/auth.d.ts +0 -85
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -181
- package/dist/auth.js.map +0 -1
- package/dist/client-info.d.ts +0 -18
- package/dist/client-info.d.ts.map +0 -1
- package/dist/client-info.js +0 -25
- package/dist/client-info.js.map +0 -1
- package/dist/event-emitter.d.ts +0 -45
- package/dist/event-emitter.d.ts.map +0 -1
- package/dist/event-emitter.js +0 -86
- package/dist/event-emitter.js.map +0 -1
- package/dist/graphql.d.ts +0 -90
- package/dist/graphql.d.ts.map +0 -1
- package/dist/graphql.js +0 -463
- package/dist/graphql.js.map +0 -1
- package/dist/logger.d.ts +0 -13
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -17
- package/dist/logger.js.map +0 -1
- package/dist/path.d.ts +0 -6
- package/dist/path.d.ts.map +0 -1
- package/dist/path.js +0 -47
- package/dist/path.js.map +0 -1
- package/dist/reroute.d.ts +0 -22
- package/dist/reroute.d.ts.map +0 -1
- package/dist/reroute.js +0 -70
- package/dist/reroute.js.map +0 -1
- package/dist/rest.d.ts +0 -54
- package/dist/rest.d.ts.map +0 -1
- package/dist/rest.js +0 -171
- package/dist/rest.js.map +0 -1
- package/dist/router.d.ts +0 -20
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js +0 -54
- package/dist/router.js.map +0 -1
- package/dist/space-session.d.ts +0 -30
- package/dist/space-session.d.ts.map +0 -1
- package/dist/space-session.js +0 -83
- package/dist/space-session.js.map +0 -1
- package/dist/space.d.ts +0 -161
- package/dist/space.d.ts.map +0 -1
- package/dist/space.js +0 -618
- package/dist/space.js.map +0 -1
- package/dist/subscription.d.ts +0 -36
- package/dist/subscription.d.ts.map +0 -1
- package/dist/subscription.js +0 -419
- package/dist/subscription.js.map +0 -1
- package/dist/webdav.d.ts +0 -171
- package/dist/webdav.d.ts.map +0 -1
- package/dist/webdav.js +0 -481
- package/dist/webdav.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,659 +1,372 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* - User storage (cross-device key-value storage)
|
|
21
|
-
*/
|
|
22
|
-
export class RoolClient extends EventEmitter {
|
|
23
|
-
baseUrl;
|
|
24
|
-
urls;
|
|
25
|
-
authManager;
|
|
26
|
-
graphqlClient;
|
|
27
|
-
router;
|
|
28
|
-
subscriptionManager = null;
|
|
29
|
-
logger;
|
|
30
|
-
clientInfo;
|
|
31
|
-
_serverInfo = null;
|
|
32
|
-
// Open spaces (cached for reuse)
|
|
33
|
-
openSpaces = new Map();
|
|
34
|
-
// User storage cache (in-memory; populated from server on initialize)
|
|
35
|
-
_storageCache = {};
|
|
36
|
-
// Current user (fetched during initialize)
|
|
37
|
-
_currentUser = null;
|
|
1
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
2
|
+
import { createRoolEvents } from "./events.js";
|
|
3
|
+
import { RoolMachine } from "./machine.js";
|
|
4
|
+
import { throwProblemResponse } from "./problem.js";
|
|
5
|
+
const DEFAULT_API_URL = "https://api.rool.dev";
|
|
6
|
+
const MACHINE_ROUTE_CHANGED_HEADER = "Rool-Machine-Route-Changed";
|
|
7
|
+
const ROUTE_CHANGE_MAX_RETRIES = 6;
|
|
8
|
+
const ROUTE_CHANGE_RETRY_BASE_MS = 150;
|
|
9
|
+
const ROUTE_CHANGE_RETRY_MAX_MS = 5_000;
|
|
10
|
+
export const roolSdkVersion = packageJson.version;
|
|
11
|
+
export class RoolClient {
|
|
12
|
+
apiUrl;
|
|
13
|
+
fetchRequest;
|
|
14
|
+
usesDefaultFetch;
|
|
15
|
+
getTokens;
|
|
16
|
+
onAuthInvalidated;
|
|
17
|
+
identity;
|
|
18
|
+
machines = new Map();
|
|
19
|
+
events;
|
|
38
20
|
constructor(config = {}) {
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const apiOrigin = ((config.apiUrl ?? config.baseUrl) ?? 'https://api.rool.dev').replace(/\/+$/, '');
|
|
47
|
-
let authOrigin = apiOrigin;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(apiOrigin);
|
|
50
|
-
if (parsed.hostname.startsWith('api.')) {
|
|
51
|
-
parsed.hostname = parsed.hostname.slice(4);
|
|
52
|
-
authOrigin = parsed.origin;
|
|
53
|
-
}
|
|
21
|
+
this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
22
|
+
this.fetchRequest = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
23
|
+
this.usesDefaultFetch = config.fetch === undefined;
|
|
24
|
+
this.identity = config.client;
|
|
25
|
+
this.onAuthInvalidated = config.onAuthInvalidated;
|
|
26
|
+
if (config.getTokens && config.getAccessToken) {
|
|
27
|
+
throw new Error("getTokens and getAccessToken cannot be used together");
|
|
54
28
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.urls = {
|
|
58
|
-
graphql: config.graphqlUrl ?? `${apiOrigin}/graphql`,
|
|
59
|
-
auth: config.authUrl ?? `${authOrigin}/auth`,
|
|
60
|
-
webdav: apiOrigin,
|
|
61
|
-
};
|
|
62
|
-
this.authManager = new AuthManager({
|
|
63
|
-
authUrl: this.urls.auth,
|
|
64
|
-
authProvider: config.authProvider,
|
|
65
|
-
logger: this.logger,
|
|
66
|
-
onAuthStateChanged: (authenticated) => {
|
|
67
|
-
// Covers every sign-out path (logout() and 401-driven token clearing).
|
|
68
|
-
if (!authenticated) {
|
|
69
|
-
this._storageCache = {};
|
|
70
|
-
this.setCurrentUser(null);
|
|
71
|
-
}
|
|
72
|
-
this.emit('authStateChanged', authenticated);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
this.graphqlClient = new GraphQLClient({
|
|
76
|
-
graphqlUrl: this.urls.graphql,
|
|
77
|
-
authManager: this.authManager,
|
|
78
|
-
clientInfo: this.clientInfo,
|
|
79
|
-
});
|
|
80
|
-
this.router = new SpaceRouter({
|
|
81
|
-
apiUrl: this.baseUrl,
|
|
82
|
-
authManager: this.authManager,
|
|
83
|
-
clientInfo: this.clientInfo,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Initialize the client - should be called on app startup.
|
|
88
|
-
* Processes any auth callback in the URL, sets up auto-refresh,
|
|
89
|
-
* and starts real-time event subscription if authenticated.
|
|
90
|
-
* @returns true if authenticated, false otherwise
|
|
91
|
-
*/
|
|
92
|
-
async initialize() {
|
|
93
|
-
this.authManager.initialize();
|
|
94
|
-
const authenticated = await this.isAuthenticated();
|
|
95
|
-
if (authenticated) {
|
|
96
|
-
await this.hydrateAuthenticatedSession();
|
|
97
|
-
}
|
|
98
|
-
return authenticated;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Start the realtime subscription and load the current user/storage. Shared
|
|
102
|
-
* by initialize() and verify() — any path that lands the user in an
|
|
103
|
-
* authenticated state needs this hydration.
|
|
104
|
-
*
|
|
105
|
-
* Hydration is best-effort and never throws: a backend outage must not read
|
|
106
|
-
* as a logout. The subscription owns reconnect/backoff and refetches the user
|
|
107
|
-
* on (re)connect (see ensureSubscribed), so a session that boots while the
|
|
108
|
-
* server is down stays authenticated and self-heals when it returns. Only a
|
|
109
|
-
* hard auth failure (401) ends the session, via the token layer.
|
|
110
|
-
*/
|
|
111
|
-
async hydrateAuthenticatedSession() {
|
|
112
|
-
this.ensureSubscribed().catch((error) => {
|
|
113
|
-
this.logger.warn('[RoolClient] subscription start deferred:', error);
|
|
114
|
-
});
|
|
115
|
-
try {
|
|
116
|
-
await this.fetchUserAndStorage();
|
|
29
|
+
if (config.getTokens) {
|
|
30
|
+
this.getTokens = async () => (await config.getTokens()) ?? undefined;
|
|
117
31
|
}
|
|
118
|
-
|
|
119
|
-
this.
|
|
32
|
+
else if (config.getAccessToken) {
|
|
33
|
+
this.getTokens = async () => {
|
|
34
|
+
const accessToken = await config.getAccessToken();
|
|
35
|
+
return accessToken ? { accessToken, roolToken: "" } : undefined;
|
|
36
|
+
};
|
|
120
37
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* Fetch the current user and populate the storage cache, then emit
|
|
124
|
-
* currentUserChanged. Cache is set before the emit so listeners reading
|
|
125
|
-
* getAllUserStorage() in response see fresh storage.
|
|
126
|
-
*/
|
|
127
|
-
async fetchUserAndStorage() {
|
|
128
|
-
const user = await this.graphqlClient.getCurrentUser();
|
|
129
|
-
this._storageCache = user.storage ?? {};
|
|
130
|
-
this.setCurrentUser(user);
|
|
131
|
-
}
|
|
132
|
-
setCurrentUser(user) {
|
|
133
|
-
this._currentUser = user;
|
|
134
|
-
this.emit('currentUserChanged', user);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Clean up resources - call when destroying the client.
|
|
138
|
-
*/
|
|
139
|
-
destroy() {
|
|
140
|
-
this.authManager.destroy();
|
|
141
|
-
this.subscriptionManager?.destroy();
|
|
142
|
-
// Close all open spaces and subscriptions
|
|
143
|
-
for (const space of this.openSpaces.values())
|
|
144
|
-
space.close();
|
|
145
|
-
this.openSpaces.clear();
|
|
146
|
-
this.removeAllListeners();
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Initiate login by redirecting to auth page.
|
|
150
|
-
* @param appName - The name of the application requesting login (displayed on auth page)
|
|
151
|
-
*/
|
|
152
|
-
async login(appName, params) {
|
|
153
|
-
return this.authManager.login(appName, params);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Initiate signup by redirecting to auth page.
|
|
157
|
-
* @param appName - The name of the application requesting signup (displayed on auth page)
|
|
158
|
-
* @param params - Optional additional query parameters to pass to the auth server
|
|
159
|
-
*/
|
|
160
|
-
async signup(appName, params) {
|
|
161
|
-
return this.authManager.signup(appName, params);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Complete an email verification flow using a token from the verification
|
|
165
|
-
* email link. Exchanges the token for a live session and signs the user in
|
|
166
|
-
* without a redirect. Intended to be called when the app detects a
|
|
167
|
-
* `?verify=<token>` query parameter on load.
|
|
168
|
-
*
|
|
169
|
-
* Returns true if the user is now signed in as a result.
|
|
170
|
-
*/
|
|
171
|
-
async verify(token) {
|
|
172
|
-
const ok = await this.authManager.verify(token);
|
|
173
|
-
if (ok) {
|
|
174
|
-
await this.hydrateAuthenticatedSession();
|
|
175
|
-
}
|
|
176
|
-
return ok;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Complete a native sign-in (PKCE) from a deep-link callback URL. Call this
|
|
180
|
-
* from the app's platform deep-link handler (e.g. Capacitor's `appUrlOpen`)
|
|
181
|
-
* with the full callback URL. Exchanges the code for a live session.
|
|
182
|
-
*
|
|
183
|
-
* Returns true if the user is now signed in as a result.
|
|
184
|
-
*/
|
|
185
|
-
async handleAuthRedirect(url) {
|
|
186
|
-
const ok = await this.authManager.handleRedirect(url);
|
|
187
|
-
if (ok) {
|
|
188
|
-
await this.hydrateAuthenticatedSession();
|
|
189
|
-
}
|
|
190
|
-
return ok;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Sign in with email + password. Resolves to `{ status: 'signed_in' }` once
|
|
194
|
-
* authenticated, or `{ status: 'verify_required' }` when the account's email
|
|
195
|
-
* isn't verified yet (a magic link has been emailed). Rejects with a
|
|
196
|
-
* human-readable Error on bad credentials or server failure.
|
|
197
|
-
*/
|
|
198
|
-
async signInWithPassword(email, password) {
|
|
199
|
-
const result = await this.authManager.signInWithPassword(email, password);
|
|
200
|
-
if (result.status === 'signed_in') {
|
|
201
|
-
await this.hydrateAuthenticatedSession();
|
|
38
|
+
else {
|
|
39
|
+
this.getTokens = async () => undefined;
|
|
202
40
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*/
|
|
211
|
-
async requestMagicLink(email) {
|
|
212
|
-
return this.authManager.requestMagicLink(email);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Set or change the current user's password. Other authenticated sessions are
|
|
216
|
-
* revoked while this client receives replacement credentials.
|
|
217
|
-
*
|
|
218
|
-
* Throws an Error with a human-readable message on validation or server failure.
|
|
219
|
-
*/
|
|
220
|
-
async setPassword(password) {
|
|
221
|
-
return this.authManager.setPassword(password);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Logout - clear all tokens and state.
|
|
225
|
-
*/
|
|
226
|
-
logout() {
|
|
227
|
-
this.authManager.logout();
|
|
228
|
-
this.unsubscribe();
|
|
229
|
-
// Close all open spaces and subscriptions
|
|
230
|
-
for (const space of this.openSpaces.values())
|
|
231
|
-
space.close();
|
|
232
|
-
this.openSpaces.clear();
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Check if user is currently authenticated (validates token is usable).
|
|
236
|
-
*/
|
|
237
|
-
async isAuthenticated() {
|
|
238
|
-
return this.authManager.isAuthenticated();
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Make an authenticated fetch request to the Rool API.
|
|
242
|
-
* @internal Not part of the public API — use typed methods instead.
|
|
243
|
-
*
|
|
244
|
-
* @param path - Path relative to the base URL (e.g., '/billing/usage')
|
|
245
|
-
* @param init - Standard fetch RequestInit options. Authorization header is added automatically.
|
|
246
|
-
*/
|
|
247
|
-
async _api(path, init) {
|
|
248
|
-
const tokens = await this.authManager.getTokens();
|
|
249
|
-
if (!tokens)
|
|
250
|
-
throw new Error('Not authenticated');
|
|
251
|
-
const headers = new Headers(init?.headers);
|
|
252
|
-
headers.set('Authorization', `Bearer ${tokens.accessToken}`);
|
|
253
|
-
headers.set('X-Rool-Token', tokens.roolToken);
|
|
254
|
-
addClientInfoHeaders(headers, this.clientInfo);
|
|
255
|
-
return fetch(`${this.baseUrl}${path}`, { ...init, headers });
|
|
41
|
+
this.events = createRoolEvents({
|
|
42
|
+
poll: (syncToken, signal) => {
|
|
43
|
+
const query = new URLSearchParams({ syncToken });
|
|
44
|
+
return this.request(`/v2/events?${query}`, { headers: { Prefer: "wait=45" }, signal }, true);
|
|
45
|
+
},
|
|
46
|
+
getSession: () => this.getSession(),
|
|
47
|
+
});
|
|
256
48
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
* For the Rool user (with server-assigned id), use currentUser.
|
|
260
|
-
*/
|
|
261
|
-
getAuthUser() {
|
|
262
|
-
return this.authManager.getAuthUser();
|
|
49
|
+
getSession() {
|
|
50
|
+
return this.requestJson("/v2/session");
|
|
263
51
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* Available after successful authentication.
|
|
267
|
-
*/
|
|
268
|
-
get currentUser() {
|
|
269
|
-
return this._currentUser;
|
|
52
|
+
getAccount() {
|
|
53
|
+
return this.requestJson("/v2/me");
|
|
270
54
|
}
|
|
271
|
-
|
|
272
|
-
|
|
55
|
+
async deleteAccount() {
|
|
56
|
+
await this.request("/v2/me", { method: "DELETE" });
|
|
273
57
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
*/
|
|
277
|
-
async listSpaces() {
|
|
278
|
-
return this.graphqlClient.listSpaces();
|
|
58
|
+
getProfile() {
|
|
59
|
+
return this.requestJson("/v2/me/profile");
|
|
279
60
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
* Call space.close() when done to stop the subscription.
|
|
286
|
-
*/
|
|
287
|
-
async openSpace(spaceId) {
|
|
288
|
-
// Reuse existing open space
|
|
289
|
-
const existing = this.openSpaces.get(spaceId);
|
|
290
|
-
if (existing)
|
|
291
|
-
return existing;
|
|
292
|
-
// Ensure client subscription is active (for lifecycle events)
|
|
293
|
-
// .catch prevents a rejection here from crashing Node before the caller awaits it.
|
|
294
|
-
this.ensureSubscribed().catch(() => { });
|
|
295
|
-
const initialRoute = await this.router.resolve(spaceId);
|
|
296
|
-
const scopedGraphqlUrl = `${initialRoute.server.replace(/\/+$/, '')}/graphql`;
|
|
297
|
-
const scopedClient = new GraphQLClient({
|
|
298
|
-
graphqlUrl: scopedGraphqlUrl,
|
|
299
|
-
authManager: this.authManager,
|
|
300
|
-
clientInfo: this.clientInfo,
|
|
301
|
-
});
|
|
302
|
-
const scopedRestClient = new RestClient({
|
|
303
|
-
apiUrl: initialRoute.server,
|
|
304
|
-
authManager: this.authManager,
|
|
305
|
-
clientInfo: this.clientInfo,
|
|
306
|
-
});
|
|
307
|
-
const openSpaceResult = await scopedClient.openSpace(spaceId);
|
|
308
|
-
const space = new RoolSpace({
|
|
309
|
-
id: spaceId,
|
|
310
|
-
openSpaceResult,
|
|
311
|
-
graphqlClient: scopedClient,
|
|
312
|
-
restClient: scopedRestClient,
|
|
313
|
-
authManager: this.authManager,
|
|
314
|
-
clientInfo: this.clientInfo,
|
|
315
|
-
router: this.router,
|
|
316
|
-
initialRoute,
|
|
317
|
-
logger: this.logger,
|
|
318
|
-
onClose: () => this.openSpaces.delete(spaceId),
|
|
61
|
+
replaceProfile(profile) {
|
|
62
|
+
return this.requestJson("/v2/me/profile", {
|
|
63
|
+
method: "PUT",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify(profile),
|
|
319
66
|
});
|
|
320
|
-
this.openSpaces.set(spaceId, space);
|
|
321
|
-
return space;
|
|
322
67
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
* Returns a RoolSpace handle with a real-time subscription.
|
|
326
|
-
* Use the returned RoolSpace to work with objects, conversations, and AI.
|
|
327
|
-
*/
|
|
328
|
-
async createSpace(name) {
|
|
329
|
-
// Prevents a rejection here from crashing Node before the caller awaits it.
|
|
330
|
-
this.ensureSubscribed().catch(() => { });
|
|
331
|
-
const { spaceId } = await this.graphqlClient.createSpace(name);
|
|
332
|
-
return this.openSpace(spaceId);
|
|
68
|
+
getUserAppData() {
|
|
69
|
+
return this.requestJson("/v2/me/app-data");
|
|
333
70
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const scopedUrl = (server) => `${server.replace(/\/+$/, '')}/graphql`;
|
|
340
|
-
const route = await this.router.resolve(spaceId);
|
|
341
|
-
return new GraphQLClient({
|
|
342
|
-
graphqlUrl: scopedUrl(route.server),
|
|
343
|
-
authManager: this.authManager,
|
|
344
|
-
clientInfo: this.clientInfo,
|
|
345
|
-
onRefused: async () => scopedUrl((await this.router.resolve(spaceId)).server),
|
|
71
|
+
async setUserAppData(key, value) {
|
|
72
|
+
await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
|
|
73
|
+
method: "PUT",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ value }),
|
|
346
76
|
});
|
|
347
77
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
async deleteSpace(spaceId) {
|
|
353
|
-
const scoped = await this.scopedGraphqlClient(spaceId);
|
|
354
|
-
await scoped.deleteSpace(spaceId);
|
|
355
|
-
// Close and remove the cached space if open
|
|
356
|
-
const space = this.openSpaces.get(spaceId);
|
|
357
|
-
if (space) {
|
|
358
|
-
space.close();
|
|
359
|
-
this.openSpaces.delete(spaceId);
|
|
360
|
-
}
|
|
361
|
-
// Client-level event will be emitted via SSE subscription
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Duplicate an existing space. Returns a handle to the new space.
|
|
365
|
-
*/
|
|
366
|
-
async duplicateSpace(sourceSpaceId, name) {
|
|
367
|
-
this.ensureSubscribed().catch(() => { });
|
|
368
|
-
const scoped = await this.scopedGraphqlClient(sourceSpaceId);
|
|
369
|
-
const { spaceId } = await scoped.duplicateSpace(sourceSpaceId, name);
|
|
370
|
-
return this.openSpace(spaceId);
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Mark the current user for deletion (7-day grace period).
|
|
374
|
-
* Irrecoverable after the grace period elapses.
|
|
375
|
-
*/
|
|
376
|
-
async deleteCurrentUser() {
|
|
377
|
-
await this.graphqlClient.deleteCurrentUser();
|
|
378
|
-
this.logout();
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Import a space from a zip archive.
|
|
382
|
-
* Creates a new space with the given name and imports objects, relations, and files.
|
|
383
|
-
* Returns a RoolSpace handle.
|
|
384
|
-
*/
|
|
385
|
-
async importArchive(name, archive) {
|
|
386
|
-
// .catch prevents a rejection here from crashing Node before the caller awaits it.
|
|
387
|
-
this.ensureSubscribed().catch(() => { });
|
|
388
|
-
// Import via REST endpoint (creates the space)
|
|
389
|
-
const spaceId = await this.restClient.importArchive(name, archive);
|
|
390
|
-
// Open the space to get its data
|
|
391
|
-
return this.openSpace(spaceId);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Get the current Rool user from the server.
|
|
395
|
-
* Returns the user's server-assigned id, email, plan, and credits.
|
|
396
|
-
*/
|
|
397
|
-
async getCurrentUser() {
|
|
398
|
-
const user = await this.graphqlClient.getCurrentUser();
|
|
399
|
-
// On first hydration, populate the storage cache before emitting so
|
|
400
|
-
// listeners don't see a user with empty storage. Once hydrated, a polled
|
|
401
|
-
// snapshot must not clobber optimistic setUserStorage writes.
|
|
402
|
-
if (!this._currentUser) {
|
|
403
|
-
this._storageCache = user.storage ?? {};
|
|
404
|
-
}
|
|
405
|
-
this.setCurrentUser(user);
|
|
406
|
-
return user;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Look up an invite link by its token, without redeeming it.
|
|
410
|
-
* Does not require authentication.
|
|
411
|
-
*/
|
|
412
|
-
async previewInvite(token) {
|
|
413
|
-
return this.restClient.previewInvite(token);
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Redeem an invite link, joining the space it belongs to.
|
|
417
|
-
*/
|
|
418
|
-
async redeemInvite(token) {
|
|
419
|
-
return this.restClient.redeemInvite(token);
|
|
78
|
+
async deleteUserAppData(key) {
|
|
79
|
+
await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
|
|
80
|
+
method: "DELETE",
|
|
81
|
+
});
|
|
420
82
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
83
|
+
getGreeting(language) {
|
|
84
|
+
if (!language)
|
|
85
|
+
return this.requestJson("/v2/greeting");
|
|
86
|
+
const query = new URLSearchParams({ language });
|
|
87
|
+
return this.requestJson(`/v2/greeting?${query}`);
|
|
88
|
+
}
|
|
89
|
+
/** Mint a short-lived key for Speechmatics real-time transcription. */
|
|
90
|
+
getSpeechmaticsToken(options = {}) {
|
|
91
|
+
return this.requestJson("/v2/speechmatics/token", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify(options),
|
|
95
|
+
});
|
|
427
96
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
* Refetches the user, since any gift kind changes their standing.
|
|
431
|
-
*/
|
|
432
|
-
async claimGift(code) {
|
|
433
|
-
const result = await this.restClient.claimGift(code);
|
|
434
|
-
// The claim is spent and irreversible by now, so a failed refresh must not
|
|
435
|
-
// surface as a failed claim: the retry would report gift_claimed.
|
|
436
|
-
try {
|
|
437
|
-
await this.getCurrentUser();
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
this.logger.warn('[RoolClient] user refresh after gift claim deferred:', error);
|
|
441
|
-
}
|
|
442
|
-
return result;
|
|
97
|
+
listMachines() {
|
|
98
|
+
return this.requestJson("/v2/machines");
|
|
443
99
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
100
|
+
createMachine(settings) {
|
|
101
|
+
return this.requestJson("/v2/machines", {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify(settings),
|
|
105
|
+
});
|
|
449
106
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
107
|
+
machine(machineId) {
|
|
108
|
+
let machine = this.machines.get(machineId);
|
|
109
|
+
if (machine)
|
|
110
|
+
return machine;
|
|
111
|
+
machine = new RoolMachine(machineId, {
|
|
112
|
+
send: (path, init, onUploadProgress) => this.send(path, init, { onUploadProgress }),
|
|
113
|
+
request: (path, init, allowHttpErrors) => this.request(path, init, allowHttpErrors),
|
|
114
|
+
requestJson: (path, init) => this.requestJson(path, init),
|
|
115
|
+
subscribeEvents: (listener) => this.events.subscribe(listener),
|
|
116
|
+
deleted: () => this.machines.delete(machineId),
|
|
117
|
+
});
|
|
118
|
+
this.machines.set(machineId, machine);
|
|
119
|
+
return machine;
|
|
456
120
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
* link stop working; the gift itself is untouched.
|
|
460
|
-
*/
|
|
461
|
-
async rotateGiftCode(giftId) {
|
|
462
|
-
return this.restClient.rotateGiftCode(giftId);
|
|
121
|
+
getInvitePreview(token) {
|
|
122
|
+
return this.requestJson(`/v2/invites/${encodeURIComponent(token)}`);
|
|
463
123
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
* - name: display name
|
|
467
|
-
* - slug: used in app publishing URLs (3-32 chars, start with letter, lowercase alphanumeric/hyphens/underscores)
|
|
468
|
-
*/
|
|
469
|
-
async updateCurrentUser(input) {
|
|
470
|
-
const user = await this.graphqlClient.updateCurrentUser(input);
|
|
471
|
-
this.setCurrentUser(user);
|
|
472
|
-
return user;
|
|
124
|
+
redeemInvite(token) {
|
|
125
|
+
return this.requestJson(`/v2/invites/${encodeURIComponent(token)}/redemption`, { method: "POST" });
|
|
473
126
|
}
|
|
474
|
-
/**
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* Returns undefined if key doesn't exist.
|
|
478
|
-
*/
|
|
479
|
-
getUserStorage(key) {
|
|
480
|
-
return this._storageCache[key];
|
|
127
|
+
/** Look up a gift by its code without claiming it. */
|
|
128
|
+
previewGift(code) {
|
|
129
|
+
return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}`);
|
|
481
130
|
}
|
|
482
|
-
/**
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
* Storage is limited to 10MB total.
|
|
487
|
-
*/
|
|
488
|
-
setUserStorage(key, value) {
|
|
489
|
-
// Update local cache
|
|
490
|
-
if (value === null || value === undefined) {
|
|
491
|
-
delete this._storageCache[key];
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
this._storageCache[key] = value;
|
|
495
|
-
}
|
|
496
|
-
// Emit event (local source)
|
|
497
|
-
this.emit('userStorageChanged', { key, value: value ?? null, source: 'local' });
|
|
498
|
-
// Fire-and-forget server sync
|
|
499
|
-
this.graphqlClient.setUserStorage(key, value).catch((error) => {
|
|
500
|
-
this.logger.error('[RoolClient] Failed to sync user storage:', error);
|
|
501
|
-
this.emit('error', error instanceof Error ? error : new Error(String(error)), 'userStorage');
|
|
131
|
+
/** Claim a gift for the current account. */
|
|
132
|
+
claimGift(code) {
|
|
133
|
+
return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}/claim`, {
|
|
134
|
+
method: "POST",
|
|
502
135
|
});
|
|
503
136
|
}
|
|
504
|
-
/**
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
getAllUserStorage() {
|
|
508
|
-
return { ...this._storageCache };
|
|
137
|
+
/** List the current user's claimed and unclaimed gifts. */
|
|
138
|
+
listGifts() {
|
|
139
|
+
return this.requestJson("/v2/me/gifts");
|
|
509
140
|
}
|
|
510
|
-
/**
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.logger.error('[RoolClient] Failed to report event:', error);
|
|
141
|
+
/** Set or clear a gift's note, or change its archived state. */
|
|
142
|
+
updateGift(giftId, changes) {
|
|
143
|
+
return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}`, {
|
|
144
|
+
method: "PATCH",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify(changes),
|
|
517
147
|
});
|
|
518
148
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
clientInfo: this.clientInfo,
|
|
149
|
+
/** Mint a new code for an unclaimed gift. */
|
|
150
|
+
rotateGiftCode(giftId) {
|
|
151
|
+
return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}/code`, {
|
|
152
|
+
method: "POST",
|
|
524
153
|
});
|
|
525
154
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
onError: (error) => {
|
|
552
|
-
this.emit('error', error, 'subscription');
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
await this.subscriptionManager.subscribe();
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Disconnect from real-time events.
|
|
559
|
-
* @internal
|
|
560
|
-
*/
|
|
561
|
-
unsubscribe() {
|
|
562
|
-
if (this.subscriptionManager) {
|
|
563
|
-
this.subscriptionManager.destroy();
|
|
564
|
-
this.subscriptionManager = null;
|
|
155
|
+
async requestJson(path, init) {
|
|
156
|
+
const response = await this.request(path, init);
|
|
157
|
+
return response.json();
|
|
158
|
+
}
|
|
159
|
+
async request(path, init, allowHttpErrors = false) {
|
|
160
|
+
const accept = new Headers(init?.headers).get("Accept") ?? "application/json";
|
|
161
|
+
const response = await this.send(path, init, { accept });
|
|
162
|
+
if (response.status === 401)
|
|
163
|
+
void this.onAuthInvalidated?.();
|
|
164
|
+
if (response.ok || allowHttpErrors)
|
|
165
|
+
return response;
|
|
166
|
+
return throwProblemResponse(response);
|
|
167
|
+
}
|
|
168
|
+
async send(path, init, options = {}) {
|
|
169
|
+
const signal = init?.signal;
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
const headers = new Headers(init?.headers);
|
|
172
|
+
if (options.accept)
|
|
173
|
+
headers.set("Accept", options.accept);
|
|
174
|
+
headers.set("X-Rool-SDK-Name", packageJson.name);
|
|
175
|
+
headers.set("X-Rool-SDK-Version", packageJson.version);
|
|
176
|
+
if (this.identity?.appName) {
|
|
177
|
+
headers.set("X-Rool-App-Name", this.identity.appName);
|
|
565
178
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
179
|
+
if (this.identity?.appVersion) {
|
|
180
|
+
headers.set("X-Rool-App-Version", this.identity.appVersion);
|
|
181
|
+
}
|
|
182
|
+
if (this.identity?.osVersion) {
|
|
183
|
+
headers.set("X-Rool-OS-Version", this.identity.osVersion);
|
|
184
|
+
}
|
|
185
|
+
const tokens = await this.getTokens();
|
|
186
|
+
signal?.throwIfAborted();
|
|
187
|
+
if (tokens?.accessToken) {
|
|
188
|
+
headers.set("Authorization", `Bearer ${tokens.accessToken}`);
|
|
189
|
+
}
|
|
190
|
+
if (tokens?.roolToken)
|
|
191
|
+
headers.set("X-Rool-Token", tokens.roolToken);
|
|
192
|
+
const url = `${this.apiUrl}${path}`;
|
|
193
|
+
const configuredBody = init?.body;
|
|
194
|
+
const bodyFactory = typeof configuredBody === "function"
|
|
195
|
+
? configuredBody
|
|
196
|
+
: () => configuredBody;
|
|
197
|
+
const bodyIsReplayable = typeof configuredBody === "function" || !isReadableStream(configuredBody);
|
|
198
|
+
for (let retry = 0;; retry++) {
|
|
199
|
+
signal?.throwIfAborted();
|
|
200
|
+
const body = bodyFactory();
|
|
201
|
+
const requestInit = {
|
|
202
|
+
...init,
|
|
203
|
+
headers,
|
|
204
|
+
body,
|
|
205
|
+
};
|
|
206
|
+
if (isReadableStream(body))
|
|
207
|
+
requestInit.duplex = "half";
|
|
208
|
+
let response;
|
|
209
|
+
if (!options.onUploadProgress || body === undefined || body === null) {
|
|
210
|
+
response = await this.fetchRequest(url, requestInit);
|
|
597
211
|
}
|
|
598
|
-
|
|
599
|
-
this.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
ownerId: event.ownerId ?? '',
|
|
605
|
-
size: event.size ?? 0,
|
|
606
|
-
createdAt: event.createdAt ?? new Date().toISOString(),
|
|
607
|
-
updatedAt: event.updatedAt ?? new Date().toISOString(),
|
|
608
|
-
memberCount: 1, // Creator is the only member
|
|
609
|
-
});
|
|
610
|
-
break;
|
|
611
|
-
case 'space_deleted':
|
|
612
|
-
this.emit('spaceRemoved', event.spaceId);
|
|
613
|
-
break;
|
|
614
|
-
case 'space_renamed':
|
|
615
|
-
this.emit('spaceRenamed', event.spaceId, event.name);
|
|
616
|
-
break;
|
|
617
|
-
case 'space_access_changed':
|
|
618
|
-
if (event.role === 'none') {
|
|
619
|
-
this.emit('spaceRemoved', event.spaceId);
|
|
212
|
+
else {
|
|
213
|
+
const useXmlHttpRequest = this.usesDefaultFetch &&
|
|
214
|
+
typeof XMLHttpRequest !== "undefined" &&
|
|
215
|
+
!isReadableStream(body);
|
|
216
|
+
if (useXmlHttpRequest) {
|
|
217
|
+
response = await sendXmlHttpRequest(url, requestInit, body, options.onUploadProgress);
|
|
620
218
|
}
|
|
621
219
|
else {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
name: event.name,
|
|
625
|
-
inboundEmailAddress: event.inboundEmailAddress,
|
|
626
|
-
role: event.role,
|
|
627
|
-
ownerId: event.ownerId,
|
|
628
|
-
size: event.size,
|
|
629
|
-
createdAt: event.createdAt,
|
|
630
|
-
updatedAt: event.updatedAt,
|
|
631
|
-
memberCount: event.memberCount,
|
|
632
|
-
});
|
|
220
|
+
const progressRequest = requestWithUploadProgress(url, requestInit, body, options.onUploadProgress);
|
|
221
|
+
response = await this.fetchRequest(url, progressRequest);
|
|
633
222
|
}
|
|
634
|
-
break;
|
|
635
|
-
case 'user_storage_changed':
|
|
636
|
-
this.handleUserStorageChanged(event.key, event.value);
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* Handle a user storage change from SSE (remote update).
|
|
642
|
-
* Updates cache and emits event if value actually changed.
|
|
643
|
-
* @internal
|
|
644
|
-
*/
|
|
645
|
-
handleUserStorageChanged(key, value) {
|
|
646
|
-
const currentValue = this._storageCache[key];
|
|
647
|
-
// Only update and emit if value actually changed
|
|
648
|
-
if (JSON.stringify(currentValue) !== JSON.stringify(value)) {
|
|
649
|
-
if (value === null || value === undefined) {
|
|
650
|
-
delete this._storageCache[key];
|
|
651
223
|
}
|
|
652
|
-
|
|
653
|
-
|
|
224
|
+
const routeChanged = response.status === 421 &&
|
|
225
|
+
response.headers.get(MACHINE_ROUTE_CHANGED_HEADER) === "1";
|
|
226
|
+
if (!routeChanged ||
|
|
227
|
+
!bodyIsReplayable ||
|
|
228
|
+
retry >= ROUTE_CHANGE_MAX_RETRIES) {
|
|
229
|
+
return response;
|
|
654
230
|
}
|
|
655
|
-
|
|
231
|
+
await response.body?.cancel();
|
|
232
|
+
await delay(routeChangeBackoffMs(retry), signal);
|
|
656
233
|
}
|
|
657
234
|
}
|
|
658
235
|
}
|
|
236
|
+
function routeChangeBackoffMs(retry) {
|
|
237
|
+
const ceiling = Math.min(ROUTE_CHANGE_RETRY_BASE_MS * 2 ** retry, ROUTE_CHANGE_RETRY_MAX_MS);
|
|
238
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
239
|
+
}
|
|
240
|
+
function delay(milliseconds, signal) {
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const finish = () => {
|
|
244
|
+
signal?.removeEventListener("abort", abort);
|
|
245
|
+
resolve();
|
|
246
|
+
};
|
|
247
|
+
const abort = () => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
reject(signal?.reason);
|
|
250
|
+
};
|
|
251
|
+
const timer = setTimeout(finish, milliseconds);
|
|
252
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function isReadableStream(body) {
|
|
256
|
+
return (typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
|
|
257
|
+
}
|
|
258
|
+
function requestWithUploadProgress(url, init, body, onUploadProgress) {
|
|
259
|
+
const preparedInit = { ...init };
|
|
260
|
+
if (isReadableStream(body))
|
|
261
|
+
preparedInit.duplex = "half";
|
|
262
|
+
const preparedRequest = new Request(url, preparedInit);
|
|
263
|
+
if (!preparedRequest.body)
|
|
264
|
+
throw new Error("Upload request is missing its body");
|
|
265
|
+
const totalBytes = uploadBodySize(body);
|
|
266
|
+
let transferredBytes = 0;
|
|
267
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
268
|
+
const progressBody = preparedRequest.body.pipeThrough(new TransformStream({
|
|
269
|
+
transform(chunk, controller) {
|
|
270
|
+
controller.enqueue(chunk);
|
|
271
|
+
transferredBytes += chunk.byteLength;
|
|
272
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
273
|
+
},
|
|
274
|
+
}));
|
|
275
|
+
return {
|
|
276
|
+
...init,
|
|
277
|
+
headers: preparedRequest.headers,
|
|
278
|
+
body: progressBody,
|
|
279
|
+
duplex: "half",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function sendXmlHttpRequest(url, init, body, onUploadProgress) {
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
const request = new XMLHttpRequest();
|
|
285
|
+
const signal = init.signal;
|
|
286
|
+
const totalBytes = uploadBodySize(body);
|
|
287
|
+
let transferredBytes = 0;
|
|
288
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
289
|
+
const abort = () => request.abort();
|
|
290
|
+
request.upload.onprogress = (event) => {
|
|
291
|
+
transferredBytes = event.loaded;
|
|
292
|
+
const reportedTotal = event.lengthComputable ? event.total : totalBytes;
|
|
293
|
+
reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
|
|
294
|
+
};
|
|
295
|
+
request.upload.onload = (event) => {
|
|
296
|
+
const loadedBytes = event.loaded;
|
|
297
|
+
if (loadedBytes === transferredBytes)
|
|
298
|
+
return;
|
|
299
|
+
transferredBytes = loadedBytes;
|
|
300
|
+
const reportedTotal = event.lengthComputable ? event.total : totalBytes;
|
|
301
|
+
reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
|
|
302
|
+
};
|
|
303
|
+
request.onload = () => {
|
|
304
|
+
cleanup();
|
|
305
|
+
const hasResponseBody = request.response instanceof ArrayBuffer &&
|
|
306
|
+
request.response.byteLength > 0;
|
|
307
|
+
const responseBody = hasResponseBody ? request.response : null;
|
|
308
|
+
resolve(new Response(responseBody, {
|
|
309
|
+
status: request.status,
|
|
310
|
+
statusText: request.statusText,
|
|
311
|
+
headers: parseXmlHttpRequestHeaders(request.getAllResponseHeaders()),
|
|
312
|
+
}));
|
|
313
|
+
};
|
|
314
|
+
request.onerror = () => {
|
|
315
|
+
cleanup();
|
|
316
|
+
reject(new TypeError("Network request failed"));
|
|
317
|
+
};
|
|
318
|
+
request.onabort = () => {
|
|
319
|
+
cleanup();
|
|
320
|
+
reject(signal?.reason ??
|
|
321
|
+
new DOMException("This operation was aborted", "AbortError"));
|
|
322
|
+
};
|
|
323
|
+
if (signal?.aborted) {
|
|
324
|
+
reject(signal.reason ??
|
|
325
|
+
new DOMException("This operation was aborted", "AbortError"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
request.open(init.method ?? "GET", url);
|
|
329
|
+
request.responseType = "arraybuffer";
|
|
330
|
+
request.withCredentials = init.credentials === "include";
|
|
331
|
+
new Headers(init.headers).forEach((value, name) => {
|
|
332
|
+
request.setRequestHeader(name, value);
|
|
333
|
+
});
|
|
334
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
335
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
336
|
+
request.send(body);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function reportUploadProgress(listener, transferredBytes, totalBytes) {
|
|
340
|
+
const progress = { transferredBytes };
|
|
341
|
+
if (totalBytes !== undefined)
|
|
342
|
+
progress.totalBytes = totalBytes;
|
|
343
|
+
listener(progress);
|
|
344
|
+
}
|
|
345
|
+
function uploadBodySize(body) {
|
|
346
|
+
if (typeof body === "string")
|
|
347
|
+
return new TextEncoder().encode(body).byteLength;
|
|
348
|
+
if (typeof Blob !== "undefined" && body instanceof Blob)
|
|
349
|
+
return body.size;
|
|
350
|
+
if (body instanceof ArrayBuffer)
|
|
351
|
+
return body.byteLength;
|
|
352
|
+
if (ArrayBuffer.isView(body))
|
|
353
|
+
return body.byteLength;
|
|
354
|
+
if (typeof URLSearchParams !== "undefined" &&
|
|
355
|
+
body instanceof URLSearchParams) {
|
|
356
|
+
return new TextEncoder().encode(body.toString()).byteLength;
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
function parseXmlHttpRequestHeaders(rawHeaders) {
|
|
361
|
+
const headers = new Headers();
|
|
362
|
+
for (const line of rawHeaders.split(/\r?\n/)) {
|
|
363
|
+
if (!line)
|
|
364
|
+
continue;
|
|
365
|
+
const separator = line.indexOf(":");
|
|
366
|
+
if (separator === -1)
|
|
367
|
+
throw new Error(`Invalid response header: ${line}`);
|
|
368
|
+
headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
369
|
+
}
|
|
370
|
+
return headers;
|
|
371
|
+
}
|
|
659
372
|
//# sourceMappingURL=client.js.map
|