@dxos/edge-client 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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