@dxos/edge-client 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6

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 (46) hide show
  1. package/dist/lib/{browser/chunk-LMP5TVOP.mjs → neutral/chunk-VESGVCLQ.mjs} +8 -4
  2. package/dist/lib/{browser/chunk-LMP5TVOP.mjs.map → neutral/chunk-VESGVCLQ.mjs.map} +3 -3
  3. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  4. package/dist/lib/{browser → neutral}/index.mjs +638 -324
  5. package/dist/lib/neutral/index.mjs.map +7 -0
  6. package/dist/lib/neutral/meta.json +1 -0
  7. package/dist/lib/{browser → neutral}/testing/index.mjs +1 -1
  8. package/dist/lib/{node-esm → neutral}/testing/index.mjs.map +2 -2
  9. package/dist/types/src/edge-client.d.ts +16 -15
  10. package/dist/types/src/edge-client.d.ts.map +1 -1
  11. package/dist/types/src/edge-http-client.d.ts +57 -22
  12. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  13. package/dist/types/src/edge-ws-connection.d.ts +19 -0
  14. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  15. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  16. package/dist/types/src/http-client.d.ts +10 -7
  17. package/dist/types/src/http-client.d.ts.map +1 -1
  18. package/dist/types/src/index.d.ts +4 -3
  19. package/dist/types/src/index.d.ts.map +1 -1
  20. package/dist/types/src/testing/test-utils.d.ts +3 -3
  21. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  22. package/dist/types/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +31 -24
  24. package/src/edge-client.test.ts +20 -15
  25. package/src/edge-client.ts +96 -43
  26. package/src/edge-http-client.test.ts +3 -2
  27. package/src/edge-http-client.ts +317 -73
  28. package/src/edge-ws-connection.ts +119 -6
  29. package/src/edge-ws-muxer.ts +1 -1
  30. package/src/http-client.test.ts +11 -7
  31. package/src/http-client.ts +18 -8
  32. package/src/index.ts +4 -3
  33. package/src/testing/test-utils.ts +8 -8
  34. package/src/websocket.test.ts +1 -1
  35. package/dist/lib/browser/index.mjs.map +0 -7
  36. package/dist/lib/browser/meta.json +0 -1
  37. package/dist/lib/browser/testing/index.mjs.map +0 -7
  38. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs +0 -299
  39. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs.map +0 -7
  40. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  41. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  42. package/dist/lib/node-esm/index.mjs +0 -1103
  43. package/dist/lib/node-esm/index.mjs.map +0 -7
  44. package/dist/lib/node-esm/meta.json +0 -1
  45. package/dist/lib/node-esm/testing/index.mjs +0 -186
  46. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,47 +2,62 @@
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';
9
+
10
+ import { type Context as OtelContext, propagation } from '@opentelemetry/api';
7
11
 
8
12
  import { sleep } from '@dxos/async';
9
13
  import { Context } from '@dxos/context';
14
+ import { runAndForwardErrors } from '@dxos/effect';
15
+ import { invariant } from '@dxos/invariant';
10
16
  import { type PublicKey, type SpaceId } from '@dxos/keys';
11
17
  import { log } from '@dxos/log';
12
18
  import {
13
- type CreateAgentResponseBody,
14
19
  type CreateAgentRequestBody,
20
+ type CreateAgentResponseBody,
15
21
  type CreateSpaceRequest,
16
22
  type CreateSpaceResponseBody,
23
+ EDGE_CLIENT_TAG_HEADER,
17
24
  EdgeAuthChallengeError,
18
25
  EdgeCallFailedError,
19
- type EdgeHttpResponse,
26
+ type EdgeFailure,
27
+ type EdgeStatus,
20
28
  type ExecuteWorkflowResponseBody,
29
+ type ExportBundleRequest,
30
+ type ExportBundleResponse,
31
+ type FeedProtocol,
21
32
  type GetAgentStatusResponseBody,
22
33
  type GetNotarizationResponseBody,
34
+ type ImportBundleRequest,
23
35
  type InitiateOAuthFlowRequest,
24
36
  type InitiateOAuthFlowResponse,
25
37
  type JoinSpaceRequest,
26
38
  type JoinSpaceResponseBody,
27
- type RecoverIdentityRequest,
28
- type RecoverIdentityResponseBody,
29
39
  type ObjectId,
30
40
  type PostNotarizationRequestBody,
31
- type QueueQuery,
32
- type QueryResult,
41
+ type RecoverIdentityRequest,
42
+ type RecoverIdentityResponseBody,
33
43
  type UploadFunctionRequest,
34
44
  type UploadFunctionResponseBody,
35
- type EdgeStatus,
36
45
  } from '@dxos/protocols';
46
+ import {
47
+ type QueryRequest as QueryRequestProto,
48
+ type QueryResponse as QueryResponseProto,
49
+ } from '@dxos/protocols/proto/dxos/echo/query';
50
+ import { TRACE_PROCESSOR, TRACE_SPAN_ATTRIBUTE } from '@dxos/tracing';
37
51
  import { createUrl } from '@dxos/util';
38
52
 
39
53
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
40
- import { encodeAuthHeader, HttpConfig, withLogging, withRetryConfig } from './http-client';
54
+ import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
41
55
  import { getEdgeUrlWithProtocol } from './utils';
42
56
 
43
57
  const DEFAULT_RETRY_TIMEOUT = 1500;
44
58
  const DEFAULT_RETRY_JITTER = 500;
45
59
  const DEFAULT_MAX_RETRIES_COUNT = 3;
60
+ const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
46
61
 
47
62
  export type RetryConfig = {
48
63
  /**
@@ -61,16 +76,38 @@ export type RetryConfig = {
61
76
 
62
77
  type EdgeHttpRequestArgs = {
63
78
  method: string;
64
- context?: Context;
65
79
  retry?: RetryConfig;
66
80
  body?: any;
81
+ /**
82
+ * @default true
83
+ */
84
+ json?: boolean;
85
+
86
+ /**
87
+ * Force authentication.
88
+ * This should be used for requests with large bodies to avoid sending the body twice.
89
+ * The client will call /auth endpoint to generate the auth header.
90
+ */
91
+ auth?: boolean;
67
92
  };
68
93
 
69
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
70
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
94
+ export type EdgeHttpCallArgs = Pick<EdgeHttpRequestArgs, 'retry' | 'auth'>;
95
+
96
+ export type GetCronTriggersResponse = {
97
+ cronIds: string[];
98
+ };
99
+
100
+ export type EdgeHttpClientOptions = {
101
+ /**
102
+ * Tag included in the {@link EDGE_CLIENT_TAG_HEADER} header on every request.
103
+ * Used on Edge to classify traffic for metering (e.g. `ci-e2e`).
104
+ */
105
+ clientTag?: string;
106
+ };
71
107
 
72
108
  export class EdgeHttpClient {
73
109
  private readonly _baseUrl: string;
110
+ private readonly _clientTag: string | undefined;
74
111
 
75
112
  private _edgeIdentity: EdgeIdentity | undefined;
76
113
 
@@ -79,8 +116,9 @@ export class EdgeHttpClient {
79
116
  */
80
117
  private _authHeader: string | undefined;
81
118
 
82
- constructor(baseUrl: string) {
119
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
83
120
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
121
+ this._clientTag = options?.clientTag;
84
122
  log('created', { url: this._baseUrl });
85
123
  }
86
124
 
@@ -99,23 +137,28 @@ export class EdgeHttpClient {
99
137
  // Status
100
138
  //
101
139
 
102
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
103
- return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
140
+ public async getStatus(ctx: Context, args?: EdgeHttpCallArgs): Promise<EdgeStatus> {
141
+ return this._call(ctx, new URL('/status', this.baseUrl), { ...args, method: 'GET', auth: true });
104
142
  }
105
143
 
106
144
  //
107
145
  // Agents
108
146
  //
109
147
 
110
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
111
- return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
148
+ public createAgent(
149
+ ctx: Context,
150
+ body: CreateAgentRequestBody,
151
+ args?: EdgeHttpCallArgs,
152
+ ): Promise<CreateAgentResponseBody> {
153
+ return this._call(ctx, new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
112
154
  }
113
155
 
114
156
  public getAgentStatus(
157
+ ctx: Context,
115
158
  request: { ownerIdentityKey: PublicKey },
116
- args?: EdgeHttpGetArgs,
159
+ args?: EdgeHttpCallArgs,
117
160
  ): Promise<GetAgentStatusResponseBody> {
118
- return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
161
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
119
162
  ...args,
120
163
  method: 'GET',
121
164
  });
@@ -125,16 +168,21 @@ export class EdgeHttpClient {
125
168
  // Credentials
126
169
  //
127
170
 
128
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
129
- return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
171
+ public getCredentialsForNotarization(
172
+ ctx: Context,
173
+ spaceId: SpaceId,
174
+ args?: EdgeHttpCallArgs,
175
+ ): Promise<GetNotarizationResponseBody> {
176
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
130
177
  }
131
178
 
132
179
  public async notarizeCredentials(
180
+ ctx: Context,
133
181
  spaceId: SpaceId,
134
182
  body: PostNotarizationRequestBody,
135
- args?: EdgeHttpGetArgs,
183
+ args?: EdgeHttpCallArgs,
136
184
  ): Promise<void> {
137
- await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
185
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
138
186
  }
139
187
 
140
188
  //
@@ -142,10 +190,11 @@ export class EdgeHttpClient {
142
190
  //
143
191
 
144
192
  public async recoverIdentity(
193
+ ctx: Context,
145
194
  body: RecoverIdentityRequest,
146
- args?: EdgeHttpGetArgs,
195
+ args?: EdgeHttpCallArgs,
147
196
  ): Promise<RecoverIdentityResponseBody> {
148
- return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
197
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
149
198
  }
150
199
 
151
200
  //
@@ -153,34 +202,32 @@ export class EdgeHttpClient {
153
202
  //
154
203
 
155
204
  public async joinSpaceByInvitation(
205
+ ctx: Context,
156
206
  spaceId: SpaceId,
157
207
  body: JoinSpaceRequest,
158
- args?: EdgeHttpGetArgs,
208
+ args?: EdgeHttpCallArgs,
159
209
  ): Promise<JoinSpaceResponseBody> {
160
- return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
210
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
161
211
  }
162
212
 
163
213
  //
164
214
  // OAuth and credentials
165
215
  //
166
216
 
167
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
168
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
169
- }
170
-
171
217
  public async initiateOAuthFlow(
218
+ ctx: Context,
172
219
  body: InitiateOAuthFlowRequest,
173
- args?: EdgeHttpGetArgs,
220
+ args?: EdgeHttpCallArgs,
174
221
  ): Promise<InitiateOAuthFlowResponse> {
175
- return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
222
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
176
223
  }
177
224
 
178
225
  //
179
226
  // Spaces
180
227
  //
181
228
 
182
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
183
- return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
229
+ async createSpace(ctx: Context, body: CreateSpaceRequest, args?: EdgeHttpCallArgs): Promise<CreateSpaceResponseBody> {
230
+ return this._call(ctx, new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
184
231
  }
185
232
 
186
233
  //
@@ -188,13 +235,16 @@ export class EdgeHttpClient {
188
235
  //
189
236
 
190
237
  public async queryQueue(
238
+ ctx: Context,
191
239
  subspaceTag: string,
192
240
  spaceId: SpaceId,
193
- query: QueueQuery,
194
- args?: EdgeHttpGetArgs,
195
- ): Promise<QueryResult> {
196
- const { queueId } = query;
241
+ query: FeedProtocol.QueueQuery,
242
+ args?: EdgeHttpCallArgs,
243
+ ): Promise<FeedProtocol.QueryResult> {
244
+ const queueId = query.queueIds?.[0];
245
+ invariant(queueId, 'queueId required');
197
246
  return this._call(
247
+ ctx,
198
248
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
199
249
  after: query.after,
200
250
  before: query.before,
@@ -210,13 +260,14 @@ export class EdgeHttpClient {
210
260
  }
211
261
 
212
262
  public async insertIntoQueue(
263
+ ctx: Context,
213
264
  subspaceTag: string,
214
265
  spaceId: SpaceId,
215
266
  queueId: ObjectId,
216
267
  objects: unknown[],
217
- args?: EdgeHttpGetArgs,
268
+ args?: EdgeHttpCallArgs,
218
269
  ): Promise<void> {
219
- return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
270
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
220
271
  ...args,
221
272
  body: { objects },
222
273
  method: 'POST',
@@ -224,13 +275,15 @@ export class EdgeHttpClient {
224
275
  }
225
276
 
226
277
  public async deleteFromQueue(
278
+ ctx: Context,
227
279
  subspaceTag: string,
228
280
  spaceId: SpaceId,
229
281
  queueId: ObjectId,
230
282
  objectIds: ObjectId[],
231
- args?: EdgeHttpGetArgs,
283
+ args?: EdgeHttpCallArgs,
232
284
  ): Promise<void> {
233
285
  return this._call(
286
+ ctx,
234
287
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
235
288
  ids: objectIds.join(','),
236
289
  }),
@@ -246,12 +299,69 @@ export class EdgeHttpClient {
246
299
  //
247
300
 
248
301
  public async uploadFunction(
302
+ ctx: Context,
249
303
  pathParts: { functionId?: string },
250
304
  body: UploadFunctionRequest,
251
- args?: EdgeHttpGetArgs,
305
+ args?: EdgeHttpCallArgs,
252
306
  ): Promise<UploadFunctionResponseBody> {
307
+ const formData = new FormData();
308
+ formData.append('name', body.name ?? '');
309
+ formData.append('version', body.version);
310
+ formData.append('ownerPublicKey', body.ownerPublicKey);
311
+ formData.append('entryPoint', body.entryPoint);
312
+ body.runtime && formData.append('runtime', body.runtime);
313
+ for (const [filename, content] of Object.entries(body.assets)) {
314
+ formData.append(
315
+ 'assets',
316
+ new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
317
+ filename,
318
+ );
319
+ }
320
+
253
321
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
254
- return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
322
+ return this._call(ctx, new URL(path, this.baseUrl), {
323
+ ...args,
324
+ body: formData,
325
+ method: 'PUT',
326
+ json: false,
327
+ });
328
+ }
329
+
330
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
331
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
332
+ }
333
+
334
+ public async invokeFunction(
335
+ ctx: Context,
336
+ params: {
337
+ functionId: string;
338
+ version?: string;
339
+ spaceId?: SpaceId;
340
+ cpuTimeLimit?: number;
341
+ subrequestsLimit?: number;
342
+ },
343
+ input: unknown,
344
+ args?: EdgeHttpCallArgs,
345
+ ): Promise<any> {
346
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
347
+ if (params.version) {
348
+ url.searchParams.set('version', params.version);
349
+ }
350
+ if (params.spaceId) {
351
+ url.searchParams.set('spaceId', params.spaceId.toString());
352
+ }
353
+ if (params.cpuTimeLimit) {
354
+ url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
355
+ }
356
+ if (params.subrequestsLimit) {
357
+ url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
358
+ }
359
+
360
+ return this._call(ctx, url, {
361
+ ...args,
362
+ body: input,
363
+ method: 'POST',
364
+ });
255
365
  }
256
366
 
257
367
  //
@@ -259,75 +369,152 @@ export class EdgeHttpClient {
259
369
  //
260
370
 
261
371
  public async executeWorkflow(
372
+ ctx: Context,
262
373
  spaceId: SpaceId,
263
374
  graphId: ObjectId,
264
375
  input: any,
265
- args?: EdgeHttpGetArgs,
376
+ args?: EdgeHttpCallArgs,
266
377
  ): Promise<ExecuteWorkflowResponseBody> {
267
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
378
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
268
379
  ...args,
269
380
  body: input,
270
381
  method: 'POST',
271
382
  });
272
383
  }
273
384
 
385
+ //
386
+ // Triggers
387
+ //
388
+
389
+ public async getCronTriggers(ctx: Context, spaceId: SpaceId): Promise<GetCronTriggersResponse> {
390
+ return this._call<GetCronTriggersResponse>(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), {
391
+ method: 'GET',
392
+ });
393
+ }
394
+
395
+ public async forceRunCronTrigger(ctx: Context, spaceId: SpaceId, triggerId: ObjectId) {
396
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), {
397
+ method: 'POST',
398
+ });
399
+ }
400
+
401
+ //
402
+ // Query
403
+ //
404
+
405
+ /**
406
+ * Execute a QueryAST query against a space.
407
+ */
408
+ public async execQuery(
409
+ ctx: Context,
410
+ spaceId: SpaceId,
411
+ body: QueryRequestProto,
412
+ args?: EdgeHttpCallArgs,
413
+ ): Promise<QueryResponseProto> {
414
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), { ...args, body, method: 'POST' });
415
+ }
416
+
417
+ //
418
+ // Import/Export space.
419
+ //
420
+
421
+ public async importBundle(
422
+ ctx: Context,
423
+ spaceId: SpaceId,
424
+ body: ImportBundleRequest,
425
+ args?: EdgeHttpCallArgs,
426
+ ): Promise<void> {
427
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
428
+ }
429
+
430
+ public async exportBundle(
431
+ ctx: Context,
432
+ spaceId: SpaceId,
433
+ body: ExportBundleRequest,
434
+ args?: EdgeHttpCallArgs,
435
+ ): Promise<ExportBundleResponse> {
436
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
437
+ ...args,
438
+ body,
439
+ method: 'POST',
440
+ });
441
+ }
442
+
274
443
  //
275
444
  // Internal
276
445
  //
277
446
 
278
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
279
- return pipe(
447
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
448
+ return Function.pipe(
280
449
  HttpClient.get(url),
281
450
  withLogging,
282
451
  withRetryConfig,
283
452
  Effect.provide(FetchHttpClient.layer),
284
453
  Effect.provide(HttpConfig.default),
285
454
  Effect.withSpan('EdgeHttpClient'),
286
- Effect.runPromise,
455
+ runAndForwardErrors,
287
456
  ) as T;
288
457
  }
289
458
 
290
459
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
291
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
460
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
292
461
  const shouldRetry = createRetryHandler(args);
293
- const requestContext = args.context ?? new Context();
294
462
  log('fetch', { url, request: args.body });
295
463
 
464
+ const traceHeaders = getTraceHeaders(ctx);
465
+
296
466
  let handledAuth = false;
467
+ const tryCount = 1;
297
468
  while (true) {
298
- let processingError: EdgeCallFailedError;
299
- let retryAfterHeaderValue: number = Number.NaN;
469
+ let processingError: EdgeCallFailedError | undefined = undefined;
300
470
  try {
301
- const request = createRequest(args, this._authHeader);
471
+ if (!this._authHeader && args.auth) {
472
+ const response = await fetch(new URL(`/auth`, this.baseUrl));
473
+ if (response.status === 401) {
474
+ this._authHeader = await this._handleUnauthorized(response);
475
+ }
476
+ }
477
+
478
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
479
+ log('call edge', { url, tryCount, authHeader: !!this._authHeader });
302
480
  const response = await fetch(url, request);
303
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
481
+
304
482
  if (response.ok) {
305
- const body = (await response.json()) as EdgeHttpResponse<T>;
483
+ const body = await response.clone().json();
484
+ invariant(body, 'Expected body to be present');
485
+ if (!('success' in body)) {
486
+ return body;
487
+ }
306
488
  if (body.success) {
307
489
  return body.data;
308
490
  }
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
491
  } else if (response.status === 401 && !handledAuth) {
317
492
  this._authHeader = await this._handleUnauthorized(response);
318
493
  handledAuth = true;
319
494
  continue;
495
+ }
496
+
497
+ const body: EdgeFailure =
498
+ response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
499
+
500
+ invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
501
+
502
+ if (body?.data?.type === 'auth_challenge' && typeof body?.data?.challenge === 'string') {
503
+ processingError = new EdgeAuthChallengeError(body.data.challenge, body.data);
504
+ } else if (body?.success === false) {
505
+ processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
320
506
  } else {
321
- processingError = EdgeCallFailedError.fromHttpFailure(response);
507
+ invariant(!response.ok, 'Expected response to not be ok.');
508
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
322
509
  }
323
510
  } catch (error: any) {
324
511
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
325
512
  }
326
513
 
327
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
328
- log('retrying edge request', { url, processingError });
514
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
515
+ log.verbose('retrying edge request', { url, processingError });
329
516
  } else {
330
- throw processingError;
517
+ throw processingError!;
331
518
  }
332
519
  }
333
520
  }
@@ -335,7 +522,7 @@ export class EdgeHttpClient {
335
522
  private async _handleUnauthorized(response: Response): Promise<string> {
336
523
  if (!this._edgeIdentity) {
337
524
  log.warn('unauthorized response received before identity was set');
338
- throw EdgeCallFailedError.fromHttpFailure(response);
525
+ throw await EdgeCallFailedError.fromHttpFailure(response);
339
526
  }
340
527
 
341
528
  const challenge = await handleAuthChallenge(response, this._edgeIdentity);
@@ -343,14 +530,64 @@ export class EdgeHttpClient {
343
530
  }
344
531
  }
345
532
 
346
- const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
533
+ const createRequest = (
534
+ { method, body, json = true }: EdgeHttpRequestArgs,
535
+ authHeader: string | undefined,
536
+ traceHeaders?: Record<string, string>,
537
+ clientTag?: string,
538
+ ): RequestInit => {
539
+ let requestBody: BodyInit | undefined;
540
+ const headers: HeadersInit = {};
541
+
542
+ if (json) {
543
+ requestBody = body && JSON.stringify(body);
544
+ headers['Content-Type'] = 'application/json';
545
+ } else {
546
+ requestBody = body;
547
+ }
548
+
549
+ if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
550
+ log.warn('Request with large body', { bodySize: requestBody.length });
551
+ }
552
+
553
+ if (authHeader) {
554
+ headers['Authorization'] = authHeader;
555
+ }
556
+
557
+ if (traceHeaders) {
558
+ Object.assign(headers, traceHeaders);
559
+ }
560
+
561
+ if (clientTag) {
562
+ headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
563
+ }
564
+
347
565
  return {
348
566
  method,
349
- body: body && JSON.stringify(body),
350
- headers: authHeader ? { Authorization: authHeader } : undefined,
567
+ body: requestBody,
568
+ headers,
351
569
  };
352
570
  };
353
571
 
572
+ /**
573
+ * Extract W3C Trace Context headers (traceparent/tracestate) from a DXOS Context.
574
+ */
575
+ const getTraceHeaders = (ctx: Context): Record<string, string> | undefined => {
576
+ const spanId = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
577
+ const otlpContext =
578
+ typeof spanId === 'number'
579
+ ? (TRACE_PROCESSOR.remoteTracing.getSpanContext(spanId) as OtelContext | undefined)
580
+ : undefined;
581
+
582
+ if (!otlpContext) {
583
+ return undefined;
584
+ }
585
+
586
+ const headers: Record<string, string> = {};
587
+ propagation.inject(otlpContext, headers);
588
+ return Object.keys(headers).length > 0 ? headers : undefined;
589
+ };
590
+
354
591
  /**
355
592
  * @deprecated
356
593
  */
@@ -363,7 +600,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
363
600
  const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
364
601
  const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
365
602
  const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
366
- return async (ctx: Context, retryAfter: number) => {
603
+ return async (ctx: Context, retryAfter?: number) => {
367
604
  if (++retries > maxRetries || ctx.disposed) {
368
605
  return false;
369
606
  }
@@ -378,3 +615,10 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
378
615
  return true;
379
616
  };
380
617
  };
618
+
619
+ const getFileMimeType = (filename: string) =>
620
+ ['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
621
+ ? 'application/javascript+module'
622
+ : filename.endsWith('.wasm')
623
+ ? 'application/wasm'
624
+ : 'application/octet-stream';