@dxos/edge-client 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5

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