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