@dxos/edge-client 0.8.4-main.fd6878d → 0.8.4-main.fffef41

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.
Files changed (38) hide show
  1. package/dist/lib/browser/{chunk-SUXH7FH6.mjs → chunk-VESGVCLQ.mjs} +4 -7
  2. package/dist/lib/browser/{chunk-SUXH7FH6.mjs.map → chunk-VESGVCLQ.mjs.map} +2 -2
  3. package/dist/lib/browser/edge-ws-muxer.mjs +1 -1
  4. package/dist/lib/browser/index.mjs +525 -287
  5. package/dist/lib/browser/index.mjs.map +4 -4
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/testing/index.mjs +1 -1
  8. package/dist/lib/browser/testing/index.mjs.map +2 -2
  9. package/dist/lib/node-esm/{chunk-R6K4IIBW.mjs → chunk-JTBFRYNM.mjs} +4 -7
  10. package/dist/lib/node-esm/{chunk-R6K4IIBW.mjs.map → chunk-JTBFRYNM.mjs.map} +2 -2
  11. package/dist/lib/node-esm/edge-ws-muxer.mjs +1 -1
  12. package/dist/lib/node-esm/index.mjs +525 -287
  13. package/dist/lib/node-esm/index.mjs.map +4 -4
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/testing/index.mjs +1 -1
  16. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  17. package/dist/types/src/edge-client.d.ts +14 -14
  18. package/dist/types/src/edge-client.d.ts.map +1 -1
  19. package/dist/types/src/edge-http-client.d.ts +30 -4
  20. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  21. package/dist/types/src/edge-ws-connection.d.ts +19 -0
  22. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  23. package/dist/types/src/http-client.d.ts +10 -7
  24. package/dist/types/src/http-client.d.ts.map +1 -1
  25. package/dist/types/src/index.d.ts +4 -3
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/testing/test-utils.d.ts +1 -1
  28. package/dist/types/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +15 -15
  30. package/src/edge-client.test.ts +4 -4
  31. package/src/edge-client.ts +72 -41
  32. package/src/edge-http-client.test.ts +1 -1
  33. package/src/edge-http-client.ts +188 -33
  34. package/src/edge-ws-connection.ts +118 -5
  35. package/src/http-client.test.ts +8 -5
  36. package/src/http-client.ts +18 -8
  37. package/src/index.ts +4 -3
  38. package/src/testing/test-utils.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/edge-client",
3
- "version": "0.8.4-main.fd6878d",
3
+ "version": "0.8.4-main.fffef41",
4
4
  "description": "EDGE Client",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -42,24 +42,24 @@
42
42
  "README.md"
43
43
  ],
44
44
  "dependencies": {
45
- "@effect/platform": "^0.90.2",
45
+ "@effect/platform": "^0.92.1",
46
46
  "isomorphic-ws": "^5.0.0",
47
47
  "ws": "^8.14.2",
48
- "@dxos/async": "0.8.4-main.fd6878d",
49
- "@dxos/context": "0.8.4-main.fd6878d",
50
- "@dxos/crypto": "0.8.4-main.fd6878d",
51
- "@dxos/credentials": "0.8.4-main.fd6878d",
52
- "@dxos/debug": "0.8.4-main.fd6878d",
53
- "@dxos/invariant": "0.8.4-main.fd6878d",
54
- "@dxos/keyring": "0.8.4-main.fd6878d",
55
- "@dxos/keys": "0.8.4-main.fd6878d",
56
- "@dxos/log": "0.8.4-main.fd6878d",
57
- "@dxos/node-std": "0.8.4-main.fd6878d",
58
- "@dxos/protocols": "0.8.4-main.fd6878d",
59
- "@dxos/util": "0.8.4-main.fd6878d"
48
+ "@dxos/async": "0.8.4-main.fffef41",
49
+ "@dxos/context": "0.8.4-main.fffef41",
50
+ "@dxos/credentials": "0.8.4-main.fffef41",
51
+ "@dxos/crypto": "0.8.4-main.fffef41",
52
+ "@dxos/debug": "0.8.4-main.fffef41",
53
+ "@dxos/invariant": "0.8.4-main.fffef41",
54
+ "@dxos/node-std": "0.8.4-main.fffef41",
55
+ "@dxos/log": "0.8.4-main.fffef41",
56
+ "@dxos/keys": "0.8.4-main.fffef41",
57
+ "@dxos/keyring": "0.8.4-main.fffef41",
58
+ "@dxos/protocols": "0.8.4-main.fffef41",
59
+ "@dxos/util": "0.8.4-main.fffef41"
60
60
  },
61
61
  "devDependencies": {
62
- "@dxos/test-utils": "0.8.4-main.fd6878d"
62
+ "@dxos/test-utils": "0.8.4-main.fffef41"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "effect": "^3.13.3"
@@ -41,17 +41,17 @@ describe('EdgeClient', () => {
41
41
 
42
42
  const { client } = await openNewClient(endpoint);
43
43
 
44
- expect(client.status).toBe(EdgeStatus.NOT_CONNECTED);
44
+ expect(client.status.state).toBe(EdgeStatus.ConnectionState.NOT_CONNECTED);
45
45
  admitConnection.wake();
46
- await expect.poll(() => client.status).toBe(EdgeStatus.CONNECTED);
46
+ await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.CONNECTED);
47
47
 
48
48
  admitConnection.reset();
49
49
  await closeConnection();
50
50
  expect(client.isOpen).is.true;
51
- await expect.poll(() => client.status).toBe(EdgeStatus.NOT_CONNECTED);
51
+ await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.NOT_CONNECTED);
52
52
 
53
53
  admitConnection.wake();
54
- await expect.poll(() => client.status).toBe(EdgeStatus.CONNECTED);
54
+ await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.CONNECTED);
55
55
  });
56
56
 
57
57
  test('set identity reconnects', async () => {
@@ -2,7 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Event, PersistentLifecycle, Trigger, TriggerState, scheduleMicroTask } from '@dxos/async';
5
+ import {
6
+ Event,
7
+ PersistentLifecycle,
8
+ Trigger,
9
+ TriggerState,
10
+ scheduleMicroTask,
11
+ scheduleTaskInterval,
12
+ } from '@dxos/async';
6
13
  import { type Lifecycle, Resource } from '@dxos/context';
7
14
  import { log, logInfo } from '@dxos/log';
8
15
  import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
@@ -17,9 +24,19 @@ import { getEdgeUrlWithProtocol } from './utils';
17
24
 
18
25
  const DEFAULT_TIMEOUT = 10_000;
19
26
 
27
+ // Refresh status every second: rtt, rate counters.
28
+ const STATUS_REFRESH_INTERVAL = 1000;
29
+
20
30
  export type MessageListener = (message: Message) => void;
21
31
  export type ReconnectListener = () => void;
22
32
 
33
+ export type MessengerConfig = {
34
+ socketEndpoint: string;
35
+ timeout?: number;
36
+ protocol?: Protocol;
37
+ disableAuth?: boolean;
38
+ };
39
+
23
40
  export interface EdgeConnection extends Required<Lifecycle> {
24
41
  statusChanged: Event<EdgeStatus>;
25
42
  get info(): any;
@@ -28,18 +45,11 @@ export interface EdgeConnection extends Required<Lifecycle> {
28
45
  get isOpen(): boolean;
29
46
  get status(): EdgeStatus;
30
47
  setIdentity(identity: EdgeIdentity): void;
48
+ send(message: Message): Promise<void>;
31
49
  onMessage(listener: MessageListener): () => void;
32
50
  onReconnected(listener: ReconnectListener): () => void;
33
- send(message: Message): Promise<void>;
34
51
  }
35
52
 
36
- export type MessengerConfig = {
37
- socketEndpoint: string;
38
- timeout?: number;
39
- protocol?: Protocol;
40
- disableAuth?: boolean;
41
- };
42
-
43
53
  /**
44
54
  * Messenger client for EDGE:
45
55
  * - While open, uses PersistentLifecycle to keep an open EdgeWsConnection, reconnecting on failures.
@@ -81,9 +91,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
81
91
  }
82
92
 
83
93
  get status(): EdgeStatus {
84
- return Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
85
- ? EdgeStatus.CONNECTED
86
- : EdgeStatus.NOT_CONNECTED;
94
+ return {
95
+ state:
96
+ Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
97
+ ? EdgeStatus.ConnectionState.CONNECTED
98
+ : EdgeStatus.ConnectionState.NOT_CONNECTED,
99
+ uptime: this._currentConnection?.uptime ?? 0,
100
+ rtt: this._currentConnection?.rtt ?? 0,
101
+ rateBytesUp: this._currentConnection?.uploadRate ?? 0,
102
+ rateBytesDown: this._currentConnection?.downloadRate ?? 0,
103
+ messagesSent: this._currentConnection?.messagesSent ?? 0,
104
+ messagesReceived: this._currentConnection?.messagesReceived ?? 0,
105
+ };
87
106
  }
88
107
 
89
108
  get identityKey() {
@@ -94,7 +113,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
94
113
  return this._identity.peerKey;
95
114
  }
96
115
 
97
- setIdentity(identity: EdgeIdentity): void {
116
+ setIdentity(identity: EdgeIdentity) {
98
117
  if (identity.identityKey !== this._identity.identityKey || identity.peerKey !== this._identity.peerKey) {
99
118
  log('Edge identity changed', { identity, oldIdentity: this._identity });
100
119
  this._identity = identity;
@@ -103,12 +122,36 @@ export class EdgeClient extends Resource implements EdgeConnection {
103
122
  }
104
123
  }
105
124
 
106
- public onMessage(listener: MessageListener): () => void {
125
+ /**
126
+ * Send message.
127
+ * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
128
+ */
129
+ public async send(message: Message) {
130
+ if (this._ready.state !== TriggerState.RESOLVED) {
131
+ log('waiting for websocket');
132
+ await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
133
+ }
134
+
135
+ if (!this._currentConnection) {
136
+ throw new EdgeConnectionClosedError();
137
+ }
138
+
139
+ if (
140
+ message.source &&
141
+ (message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
142
+ ) {
143
+ throw new EdgeIdentityChangedError();
144
+ }
145
+
146
+ this._currentConnection.send(message);
147
+ }
148
+
149
+ public onMessage(listener: MessageListener) {
107
150
  this._messageListeners.add(listener);
108
151
  return () => this._messageListeners.delete(listener);
109
152
  }
110
153
 
111
- public onReconnected(listener: () => void): () => void {
154
+ public onReconnected(listener: () => void) {
112
155
  this._reconnectListeners.add(listener);
113
156
  if (this._ready.state === TriggerState.RESOLVED) {
114
157
  // Microtask so that listener is always called asynchronously, no matter the state of the ready trigger
@@ -123,6 +166,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
123
166
  }
124
167
  });
125
168
  }
169
+
126
170
  return () => this._reconnectListeners.delete(listener);
127
171
  }
128
172
 
@@ -134,6 +178,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
134
178
  this._persistentLifecycle.open().catch((err) => {
135
179
  log.warn('Error while opening connection', { err });
136
180
  });
181
+
182
+ // Notify about status changes (rtt, rate counters).
183
+ scheduleTaskInterval(
184
+ this._ctx,
185
+ async () => {
186
+ if (!this._currentConnection) {
187
+ return;
188
+ }
189
+ this.statusChanged.emit(this.status);
190
+ },
191
+ STATUS_REFRESH_INTERVAL,
192
+ );
137
193
  }
138
194
 
139
195
  /**
@@ -200,7 +256,6 @@ export class EdgeClient extends Resource implements EdgeConnection {
200
256
  // Race with restartRequired so that restart is not blocked by _connect execution.
201
257
  // Wait on ready to attempt a reconnect if it times out.
202
258
  await Promise.race([this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }), restartRequired]);
203
-
204
259
  return connection;
205
260
  }
206
261
 
@@ -237,30 +292,6 @@ export class EdgeClient extends Resource implements EdgeConnection {
237
292
  }
238
293
  }
239
294
 
240
- /**
241
- * Send message.
242
- * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
243
- */
244
- public async send(message: Message): Promise<void> {
245
- if (this._ready.state !== TriggerState.RESOLVED) {
246
- log('waiting for websocket to become ready');
247
- await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
248
- }
249
-
250
- if (!this._currentConnection) {
251
- throw new EdgeConnectionClosedError();
252
- }
253
-
254
- if (
255
- message.source &&
256
- (message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
257
- ) {
258
- throw new EdgeIdentityChangedError();
259
- }
260
-
261
- this._currentConnection.send(message);
262
- }
263
-
264
295
  private async _createAuthHeader(path: string): Promise<string | undefined> {
265
296
  const httpUrl = new URL(path, this._baseHttpUrl);
266
297
  httpUrl.protocol = getEdgeUrlWithProtocol(this._baseWsUrl.toString(), 'http');
@@ -277,7 +308,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
277
308
  }
278
309
 
279
310
  const encodePresentationWsAuthHeader = (encodedPresentation: Uint8Array): string => {
280
- // = and / characters are not allowed in the WebSocket subprotocol header.
311
+ // '=' and '/' characters are not allowed in the WebSocket subprotocol header.
281
312
  const encodedToken = Buffer.from(encodedPresentation).toString('base64').replace(/=*$/, '').replaceAll('/', '|');
282
313
  return `base64url.bearer.authorization.dxos.org.${encodedToken}`;
283
314
  };
@@ -11,7 +11,7 @@ import { EdgeHttpClient } from './edge-http-client';
11
11
  const DEV_SERVER = 'https://edge.dxos.workers.dev';
12
12
 
13
13
  describe.skipIf(process.env.CI)('EdgeHttpClient', () => {
14
- it.only('should get status', async ({ expect }) => {
14
+ it.skip('should get status', async ({ expect }) => {
15
15
  const client = new EdgeHttpClient(DEV_SERVER);
16
16
  const identity = await createEphemeralEdgeIdentity();
17
17
  client.setIdentity(identity);
@@ -2,11 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FetchHttpClient, HttpClient } from '@effect/platform';
6
- import { Effect, pipe } from 'effect';
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Function from 'effect/Function';
7
9
 
8
10
  import { sleep } from '@dxos/async';
9
11
  import { Context } from '@dxos/context';
12
+ import { invariant } from '@dxos/invariant';
10
13
  import { type PublicKey, type SpaceId } from '@dxos/keys';
11
14
  import { log } from '@dxos/log';
12
15
  import {
@@ -16,11 +19,13 @@ import {
16
19
  type CreateSpaceResponseBody,
17
20
  EdgeAuthChallengeError,
18
21
  EdgeCallFailedError,
19
- type EdgeHttpResponse,
20
22
  type EdgeStatus,
21
23
  type ExecuteWorkflowResponseBody,
24
+ type ExportBundleRequest,
25
+ type ExportBundleResponse,
22
26
  type GetAgentStatusResponseBody,
23
27
  type GetNotarizationResponseBody,
28
+ type ImportBundleRequest,
24
29
  type InitiateOAuthFlowRequest,
25
30
  type InitiateOAuthFlowResponse,
26
31
  type JoinSpaceRequest,
@@ -43,6 +48,7 @@ import { getEdgeUrlWithProtocol } from './utils';
43
48
  const DEFAULT_RETRY_TIMEOUT = 1500;
44
49
  const DEFAULT_RETRY_JITTER = 500;
45
50
  const DEFAULT_MAX_RETRIES_COUNT = 3;
51
+ const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
46
52
 
47
53
  export type RetryConfig = {
48
54
  /**
@@ -64,10 +70,27 @@ type EdgeHttpRequestArgs = {
64
70
  context?: Context;
65
71
  retry?: RetryConfig;
66
72
  body?: any;
73
+ /**
74
+ * @default true
75
+ */
76
+ json?: boolean;
77
+
78
+ /**
79
+ * Do not expect a standard EDGE JSON response with a `success` field.
80
+ * @deprecated Use only for debugging.
81
+ */
82
+ rawResponse?: boolean;
83
+
84
+ /**
85
+ * Force authentication.
86
+ * This should be used for requests with large bodies to avoid sending the body twice.
87
+ * The client will call /auth endpoint to generate the auth header.
88
+ */
89
+ auth?: boolean;
67
90
  };
68
91
 
69
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
70
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
92
+ export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'auth'>;
93
+ export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body' | 'auth'>;
71
94
 
72
95
  export class EdgeHttpClient {
73
96
  private readonly _baseUrl: string;
@@ -164,10 +187,6 @@ export class EdgeHttpClient {
164
187
  // OAuth and credentials
165
188
  //
166
189
 
167
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
168
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
169
- }
170
-
171
190
  public async initiateOAuthFlow(
172
191
  body: InitiateOAuthFlowRequest,
173
192
  args?: EdgeHttpGetArgs,
@@ -250,8 +269,64 @@ export class EdgeHttpClient {
250
269
  body: UploadFunctionRequest,
251
270
  args?: EdgeHttpGetArgs,
252
271
  ): Promise<UploadFunctionResponseBody> {
272
+ const formData = new FormData();
273
+ formData.append('name', body.name ?? '');
274
+ formData.append('version', body.version);
275
+ formData.append('ownerPublicKey', body.ownerPublicKey);
276
+ formData.append('entryPoint', body.entryPoint);
277
+ body.runtime && formData.append('runtime', body.runtime);
278
+ for (const [filename, content] of Object.entries(body.assets)) {
279
+ formData.append(
280
+ 'assets',
281
+ new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
282
+ filename,
283
+ );
284
+ }
285
+
253
286
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
254
- return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
287
+ return this._call(new URL(path, this.baseUrl), {
288
+ ...args,
289
+ body: formData,
290
+ method: 'PUT',
291
+ json: false,
292
+ });
293
+ }
294
+
295
+ public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
296
+ return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
297
+ }
298
+
299
+ public async invokeFunction(
300
+ params: {
301
+ functionId: string;
302
+ version?: string;
303
+ spaceId?: SpaceId;
304
+ cpuTimeLimit?: number;
305
+ subrequestsLimit?: number;
306
+ },
307
+ input: unknown,
308
+ args?: EdgeHttpGetArgs,
309
+ ): Promise<any> {
310
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
311
+ if (params.version) {
312
+ url.searchParams.set('version', params.version);
313
+ }
314
+ if (params.spaceId) {
315
+ url.searchParams.set('spaceId', params.spaceId.toString());
316
+ }
317
+ if (params.cpuTimeLimit) {
318
+ url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
319
+ }
320
+ if (params.subrequestsLimit) {
321
+ url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
322
+ }
323
+
324
+ return this._call(url, {
325
+ ...args,
326
+ body: input,
327
+ method: 'POST',
328
+ rawResponse: true,
329
+ });
255
330
  }
256
331
 
257
332
  //
@@ -271,12 +346,44 @@ export class EdgeHttpClient {
271
346
  });
272
347
  }
273
348
 
349
+ //
350
+ // Triggers
351
+ //
352
+
353
+ public async getCronTriggers(spaceId: SpaceId) {
354
+ return this._call(new URL(`/test/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: 'GET' });
355
+ }
356
+
357
+ //
358
+ // Import/Export space.
359
+ //
360
+
361
+ public async importBundle(
362
+ spaceId: SpaceId, //
363
+ body: ImportBundleRequest,
364
+ args?: EdgeHttpGetArgs,
365
+ ): Promise<void> {
366
+ return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
367
+ }
368
+
369
+ public async exportBundle(
370
+ spaceId: SpaceId,
371
+ body: ExportBundleRequest,
372
+ args?: EdgeHttpGetArgs,
373
+ ): Promise<ExportBundleResponse> {
374
+ return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
375
+ ...args,
376
+ body,
377
+ method: 'POST',
378
+ });
379
+ }
380
+
274
381
  //
275
382
  // Internal
276
383
  //
277
384
 
278
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
279
- return pipe(
385
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
386
+ return Function.pipe(
280
387
  HttpClient.get(url),
281
388
  withLogging,
282
389
  withRetryConfig,
@@ -290,44 +397,64 @@ export class EdgeHttpClient {
290
397
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
291
398
  private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
292
399
  const shouldRetry = createRetryHandler(args);
293
- const requestContext = args.context ?? new Context();
400
+ const requestContext = args.context ?? Context.default();
294
401
  log('fetch', { url, request: args.body });
295
402
 
296
403
  let handledAuth = false;
404
+ const tryCount = 1;
297
405
  while (true) {
298
- let processingError: EdgeCallFailedError;
299
- let retryAfterHeaderValue: number = Number.NaN;
406
+ let processingError: EdgeCallFailedError | undefined = undefined;
300
407
  try {
408
+ if (!this._authHeader && args.auth) {
409
+ const response = await fetch(new URL(`/auth`, this.baseUrl));
410
+ if (response.status === 401) {
411
+ this._authHeader = await this._handleUnauthorized(response);
412
+ }
413
+ }
414
+
301
415
  const request = createRequest(args, this._authHeader);
416
+ log('call edge', { url, tryCount, authHeader: !!this._authHeader });
302
417
  const response = await fetch(url, request);
303
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
418
+
304
419
  if (response.ok) {
305
- const body = (await response.json()) as EdgeHttpResponse<T>;
420
+ const body = await response.clone().json();
421
+ if (args.rawResponse) {
422
+ return body as any;
423
+ }
424
+ invariant(body, 'Expected body to be present');
425
+ if (!('success' in body)) {
426
+ return body;
427
+ }
306
428
  if (body.success) {
307
429
  return body.data;
308
430
  }
309
-
310
- log.warn('unsuccessful edge response', { url, body });
311
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
312
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
313
- } else {
314
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
315
- }
316
431
  } else if (response.status === 401 && !handledAuth) {
317
432
  this._authHeader = await this._handleUnauthorized(response);
318
433
  handledAuth = true;
319
434
  continue;
435
+ }
436
+
437
+ const body =
438
+ response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
439
+
440
+ invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
441
+
442
+ if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
443
+ processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
444
+ } else if (body?.success === false) {
445
+ processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
320
446
  } else {
321
- processingError = EdgeCallFailedError.fromHttpFailure(response);
447
+ invariant(!response.ok, 'Expected response to not be ok.');
448
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
322
449
  }
323
450
  } catch (error: any) {
324
451
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
325
452
  }
326
453
 
327
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
328
- log('retrying edge request', { url, processingError });
454
+ if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
455
+ log.verbose('retrying edge request', { url, processingError });
329
456
  } else {
330
- throw processingError;
457
+ throw processingError!;
331
458
  }
332
459
  }
333
460
  }
@@ -335,7 +462,7 @@ export class EdgeHttpClient {
335
462
  private async _handleUnauthorized(response: Response): Promise<string> {
336
463
  if (!this._edgeIdentity) {
337
464
  log.warn('unauthorized response received before identity was set');
338
- throw EdgeCallFailedError.fromHttpFailure(response);
465
+ throw await EdgeCallFailedError.fromHttpFailure(response);
339
466
  }
340
467
 
341
468
  const challenge = await handleAuthChallenge(response, this._edgeIdentity);
@@ -343,11 +470,32 @@ export class EdgeHttpClient {
343
470
  }
344
471
  }
345
472
 
346
- const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
473
+ const createRequest = (
474
+ { method, body, json = true }: EdgeHttpRequestArgs,
475
+ authHeader: string | undefined,
476
+ ): RequestInit => {
477
+ let requestBody: BodyInit | undefined;
478
+ const headers: HeadersInit = {};
479
+
480
+ if (json) {
481
+ requestBody = body && JSON.stringify(body);
482
+ headers['Content-Type'] = 'application/json';
483
+ } else {
484
+ requestBody = body;
485
+ }
486
+
487
+ if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
488
+ log.warn('Request with large body', { bodySize: requestBody.length });
489
+ }
490
+
491
+ if (authHeader) {
492
+ headers['Authorization'] = authHeader;
493
+ }
494
+
347
495
  return {
348
496
  method,
349
- body: body && JSON.stringify(body),
350
- headers: authHeader ? { Authorization: authHeader } : undefined,
497
+ body: requestBody,
498
+ headers,
351
499
  };
352
500
  };
353
501
 
@@ -363,7 +511,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
363
511
  const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
364
512
  const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
365
513
  const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
366
- return async (ctx: Context, retryAfter: number) => {
514
+ return async (ctx: Context, retryAfter?: number) => {
367
515
  if (++retries > maxRetries || ctx.disposed) {
368
516
  return false;
369
517
  }
@@ -378,3 +526,10 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
378
526
  return true;
379
527
  };
380
528
  };
529
+
530
+ const getFileMimeType = (filename: string) =>
531
+ ['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
532
+ ? 'application/javascript+module'
533
+ : filename.endsWith('.wasm')
534
+ ? 'application/wasm'
535
+ : 'application/octet-stream';