@dxos/edge-client 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef

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