@dxos/edge-client 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0

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 (37) hide show
  1. package/dist/lib/{browser → neutral}/index.mjs +155 -96
  2. package/dist/lib/neutral/index.mjs.map +7 -0
  3. package/dist/lib/neutral/meta.json +1 -0
  4. package/dist/lib/{browser → neutral}/testing/index.mjs.map +1 -1
  5. package/dist/types/src/edge-client.d.ts +5 -2
  6. package/dist/types/src/edge-client.d.ts.map +1 -1
  7. package/dist/types/src/edge-http-client.d.ts +42 -33
  8. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  9. package/dist/types/src/edge-ws-connection.d.ts +1 -0
  10. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  11. package/dist/types/src/testing/test-utils.d.ts +2 -2
  12. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  13. package/dist/types/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +28 -24
  15. package/src/edge-client.test.ts +16 -11
  16. package/src/edge-client.ts +32 -3
  17. package/src/edge-http-client.test.ts +2 -1
  18. package/src/edge-http-client.ts +152 -64
  19. package/src/edge-ws-connection.ts +2 -1
  20. package/src/http-client.test.ts +3 -2
  21. package/src/testing/test-utils.ts +4 -4
  22. package/dist/lib/browser/index.mjs.map +0 -7
  23. package/dist/lib/browser/meta.json +0 -1
  24. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs +0 -303
  25. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs.map +0 -7
  26. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  27. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  28. package/dist/lib/node-esm/index.mjs +0 -1363
  29. package/dist/lib/node-esm/index.mjs.map +0 -7
  30. package/dist/lib/node-esm/meta.json +0 -1
  31. package/dist/lib/node-esm/testing/index.mjs +0 -186
  32. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  33. /package/dist/lib/{browser → neutral}/chunk-VESGVCLQ.mjs +0 -0
  34. /package/dist/lib/{browser → neutral}/chunk-VESGVCLQ.mjs.map +0 -0
  35. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +0 -0
  36. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
  37. /package/dist/lib/{browser → neutral}/testing/index.mjs +0 -0
@@ -4,11 +4,13 @@
4
4
 
5
5
  import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
6
  import * as HttpClient from '@effect/platform/HttpClient';
7
+ import { type Context as OtelContext, propagation } from '@opentelemetry/api';
7
8
  import * as Effect from 'effect/Effect';
8
9
  import * as Function from 'effect/Function';
9
10
 
10
11
  import { sleep } from '@dxos/async';
11
12
  import { Context } from '@dxos/context';
13
+ import { runAndForwardErrors } from '@dxos/effect';
12
14
  import { invariant } from '@dxos/invariant';
13
15
  import { type PublicKey, type SpaceId } from '@dxos/keys';
14
16
  import { log } from '@dxos/log';
@@ -17,12 +19,15 @@ import {
17
19
  type CreateAgentResponseBody,
18
20
  type CreateSpaceRequest,
19
21
  type CreateSpaceResponseBody,
22
+ EDGE_CLIENT_TAG_HEADER,
20
23
  EdgeAuthChallengeError,
21
24
  EdgeCallFailedError,
25
+ type EdgeFailure,
22
26
  type EdgeStatus,
23
27
  type ExecuteWorkflowResponseBody,
24
28
  type ExportBundleRequest,
25
29
  type ExportBundleResponse,
30
+ type FeedProtocol,
26
31
  type GetAgentStatusResponseBody,
27
32
  type GetNotarizationResponseBody,
28
33
  type ImportBundleRequest,
@@ -32,13 +37,16 @@ import {
32
37
  type JoinSpaceResponseBody,
33
38
  type ObjectId,
34
39
  type PostNotarizationRequestBody,
35
- type QueryResult,
36
- type QueueQuery,
37
40
  type RecoverIdentityRequest,
38
41
  type RecoverIdentityResponseBody,
39
42
  type UploadFunctionRequest,
40
43
  type UploadFunctionResponseBody,
41
44
  } from '@dxos/protocols';
45
+ import {
46
+ type QueryRequest as QueryRequestProto,
47
+ type QueryResponse as QueryResponseProto,
48
+ } from '@dxos/protocols/proto/dxos/echo/query';
49
+ import { TRACE_PROCESSOR, TRACE_SPAN_ATTRIBUTE } from '@dxos/tracing';
42
50
  import { createUrl } from '@dxos/util';
43
51
 
44
52
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
@@ -67,7 +75,6 @@ export type RetryConfig = {
67
75
 
68
76
  type EdgeHttpRequestArgs = {
69
77
  method: string;
70
- context?: Context;
71
78
  retry?: RetryConfig;
72
79
  body?: any;
73
80
  /**
@@ -75,12 +82,6 @@ type EdgeHttpRequestArgs = {
75
82
  */
76
83
  json?: boolean;
77
84
 
78
- /**
79
- * Do not expect a standard EDGE JSON response with a `success` field.
80
- * @deprecated Use only for debugging.
81
- */
82
- rawResponse?: boolean;
83
-
84
85
  /**
85
86
  * Force authentication.
86
87
  * This should be used for requests with large bodies to avoid sending the body twice.
@@ -89,11 +90,23 @@ type EdgeHttpRequestArgs = {
89
90
  auth?: boolean;
90
91
  };
91
92
 
92
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'auth'>;
93
- export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body' | 'auth'>;
93
+ export type EdgeHttpCallArgs = Pick<EdgeHttpRequestArgs, 'retry' | 'auth'>;
94
+
95
+ export type GetCronTriggersResponse = {
96
+ cronIds: string[];
97
+ };
98
+
99
+ export type EdgeHttpClientOptions = {
100
+ /**
101
+ * Tag included in the {@link EDGE_CLIENT_TAG_HEADER} header on every request.
102
+ * Used on Edge to classify traffic for metering (e.g. `ci-e2e`).
103
+ */
104
+ clientTag?: string;
105
+ };
94
106
 
95
107
  export class EdgeHttpClient {
96
108
  private readonly _baseUrl: string;
109
+ private readonly _clientTag: string | undefined;
97
110
 
98
111
  private _edgeIdentity: EdgeIdentity | undefined;
99
112
 
@@ -102,8 +115,9 @@ export class EdgeHttpClient {
102
115
  */
103
116
  private _authHeader: string | undefined;
104
117
 
105
- constructor(baseUrl: string) {
118
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
106
119
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
120
+ this._clientTag = options?.clientTag;
107
121
  log('created', { url: this._baseUrl });
108
122
  }
109
123
 
@@ -122,23 +136,28 @@ export class EdgeHttpClient {
122
136
  // Status
123
137
  //
124
138
 
125
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
126
- return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
139
+ public async getStatus(ctx: Context, args?: EdgeHttpCallArgs): Promise<EdgeStatus> {
140
+ return this._call(ctx, new URL('/status', this.baseUrl), { ...args, method: 'GET', auth: true });
127
141
  }
128
142
 
129
143
  //
130
144
  // Agents
131
145
  //
132
146
 
133
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
134
- return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
147
+ public createAgent(
148
+ ctx: Context,
149
+ body: CreateAgentRequestBody,
150
+ args?: EdgeHttpCallArgs,
151
+ ): Promise<CreateAgentResponseBody> {
152
+ return this._call(ctx, new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
135
153
  }
136
154
 
137
155
  public getAgentStatus(
156
+ ctx: Context,
138
157
  request: { ownerIdentityKey: PublicKey },
139
- args?: EdgeHttpGetArgs,
158
+ args?: EdgeHttpCallArgs,
140
159
  ): Promise<GetAgentStatusResponseBody> {
141
- return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
160
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
142
161
  ...args,
143
162
  method: 'GET',
144
163
  });
@@ -148,16 +167,21 @@ export class EdgeHttpClient {
148
167
  // Credentials
149
168
  //
150
169
 
151
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
152
- return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
170
+ public getCredentialsForNotarization(
171
+ ctx: Context,
172
+ spaceId: SpaceId,
173
+ args?: EdgeHttpCallArgs,
174
+ ): Promise<GetNotarizationResponseBody> {
175
+ return this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
153
176
  }
154
177
 
155
178
  public async notarizeCredentials(
179
+ ctx: Context,
156
180
  spaceId: SpaceId,
157
181
  body: PostNotarizationRequestBody,
158
- args?: EdgeHttpGetArgs,
182
+ args?: EdgeHttpCallArgs,
159
183
  ): Promise<void> {
160
- await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
184
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
161
185
  }
162
186
 
163
187
  //
@@ -165,10 +189,11 @@ export class EdgeHttpClient {
165
189
  //
166
190
 
167
191
  public async recoverIdentity(
192
+ ctx: Context,
168
193
  body: RecoverIdentityRequest,
169
- args?: EdgeHttpGetArgs,
194
+ args?: EdgeHttpCallArgs,
170
195
  ): Promise<RecoverIdentityResponseBody> {
171
- return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
196
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
172
197
  }
173
198
 
174
199
  //
@@ -176,11 +201,12 @@ export class EdgeHttpClient {
176
201
  //
177
202
 
178
203
  public async joinSpaceByInvitation(
204
+ ctx: Context,
179
205
  spaceId: SpaceId,
180
206
  body: JoinSpaceRequest,
181
- args?: EdgeHttpGetArgs,
207
+ args?: EdgeHttpCallArgs,
182
208
  ): Promise<JoinSpaceResponseBody> {
183
- return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
209
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
184
210
  }
185
211
 
186
212
  //
@@ -188,18 +214,19 @@ export class EdgeHttpClient {
188
214
  //
189
215
 
190
216
  public async initiateOAuthFlow(
217
+ ctx: Context,
191
218
  body: InitiateOAuthFlowRequest,
192
- args?: EdgeHttpGetArgs,
219
+ args?: EdgeHttpCallArgs,
193
220
  ): Promise<InitiateOAuthFlowResponse> {
194
- return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
221
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
195
222
  }
196
223
 
197
224
  //
198
225
  // Spaces
199
226
  //
200
227
 
201
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
202
- return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
228
+ async createSpace(ctx: Context, body: CreateSpaceRequest, args?: EdgeHttpCallArgs): Promise<CreateSpaceResponseBody> {
229
+ return this._call(ctx, new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
203
230
  }
204
231
 
205
232
  //
@@ -207,13 +234,16 @@ export class EdgeHttpClient {
207
234
  //
208
235
 
209
236
  public async queryQueue(
237
+ ctx: Context,
210
238
  subspaceTag: string,
211
239
  spaceId: SpaceId,
212
- query: QueueQuery,
213
- args?: EdgeHttpGetArgs,
214
- ): Promise<QueryResult> {
215
- const { queueId } = query;
240
+ query: FeedProtocol.QueueQuery,
241
+ args?: EdgeHttpCallArgs,
242
+ ): Promise<FeedProtocol.QueryResult> {
243
+ const queueId = query.queueIds?.[0];
244
+ invariant(queueId, 'queueId required');
216
245
  return this._call(
246
+ ctx,
217
247
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
218
248
  after: query.after,
219
249
  before: query.before,
@@ -229,13 +259,14 @@ export class EdgeHttpClient {
229
259
  }
230
260
 
231
261
  public async insertIntoQueue(
262
+ ctx: Context,
232
263
  subspaceTag: string,
233
264
  spaceId: SpaceId,
234
265
  queueId: ObjectId,
235
266
  objects: unknown[],
236
- args?: EdgeHttpGetArgs,
267
+ args?: EdgeHttpCallArgs,
237
268
  ): Promise<void> {
238
- return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
269
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
239
270
  ...args,
240
271
  body: { objects },
241
272
  method: 'POST',
@@ -243,13 +274,15 @@ export class EdgeHttpClient {
243
274
  }
244
275
 
245
276
  public async deleteFromQueue(
277
+ ctx: Context,
246
278
  subspaceTag: string,
247
279
  spaceId: SpaceId,
248
280
  queueId: ObjectId,
249
281
  objectIds: ObjectId[],
250
- args?: EdgeHttpGetArgs,
282
+ args?: EdgeHttpCallArgs,
251
283
  ): Promise<void> {
252
284
  return this._call(
285
+ ctx,
253
286
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
254
287
  ids: objectIds.join(','),
255
288
  }),
@@ -265,9 +298,10 @@ export class EdgeHttpClient {
265
298
  //
266
299
 
267
300
  public async uploadFunction(
301
+ ctx: Context,
268
302
  pathParts: { functionId?: string },
269
303
  body: UploadFunctionRequest,
270
- args?: EdgeHttpGetArgs,
304
+ args?: EdgeHttpCallArgs,
271
305
  ): Promise<UploadFunctionResponseBody> {
272
306
  const formData = new FormData();
273
307
  formData.append('name', body.name ?? '');
@@ -284,7 +318,7 @@ export class EdgeHttpClient {
284
318
  }
285
319
 
286
320
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
287
- return this._call(new URL(path, this.baseUrl), {
321
+ return this._call(ctx, new URL(path, this.baseUrl), {
288
322
  ...args,
289
323
  body: formData,
290
324
  method: 'PUT',
@@ -292,11 +326,12 @@ export class EdgeHttpClient {
292
326
  });
293
327
  }
294
328
 
295
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
296
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
329
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
330
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
297
331
  }
298
332
 
299
333
  public async invokeFunction(
334
+ ctx: Context,
300
335
  params: {
301
336
  functionId: string;
302
337
  version?: string;
@@ -305,7 +340,7 @@ export class EdgeHttpClient {
305
340
  subrequestsLimit?: number;
306
341
  },
307
342
  input: unknown,
308
- args?: EdgeHttpGetArgs,
343
+ args?: EdgeHttpCallArgs,
309
344
  ): Promise<any> {
310
345
  const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
311
346
  if (params.version) {
@@ -321,11 +356,10 @@ export class EdgeHttpClient {
321
356
  url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
322
357
  }
323
358
 
324
- return this._call(url, {
359
+ return this._call(ctx, url, {
325
360
  ...args,
326
361
  body: input,
327
362
  method: 'POST',
328
- rawResponse: true,
329
363
  });
330
364
  }
331
365
 
@@ -334,12 +368,13 @@ export class EdgeHttpClient {
334
368
  //
335
369
 
336
370
  public async executeWorkflow(
371
+ ctx: Context,
337
372
  spaceId: SpaceId,
338
373
  graphId: ObjectId,
339
374
  input: any,
340
- args?: EdgeHttpGetArgs,
375
+ args?: EdgeHttpCallArgs,
341
376
  ): Promise<ExecuteWorkflowResponseBody> {
342
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
377
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
343
378
  ...args,
344
379
  body: input,
345
380
  method: 'POST',
@@ -350,8 +385,32 @@ export class EdgeHttpClient {
350
385
  // Triggers
351
386
  //
352
387
 
353
- public async getCronTriggers(spaceId: SpaceId) {
354
- return this._call(new URL(`/test/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: 'GET' });
388
+ public async getCronTriggers(ctx: Context, spaceId: SpaceId): Promise<GetCronTriggersResponse> {
389
+ return this._call<GetCronTriggersResponse>(ctx, new URL(`/functions/${spaceId}/triggers/crons`, this.baseUrl), {
390
+ method: 'GET',
391
+ });
392
+ }
393
+
394
+ public async forceRunCronTrigger(ctx: Context, spaceId: SpaceId, triggerId: ObjectId) {
395
+ return this._call(ctx, new URL(`/functions/${spaceId}/triggers/crons/${triggerId}/run`, this.baseUrl), {
396
+ method: 'POST',
397
+ });
398
+ }
399
+
400
+ //
401
+ // Query
402
+ //
403
+
404
+ /**
405
+ * Execute a QueryAST query against a space.
406
+ */
407
+ public async execQuery(
408
+ ctx: Context,
409
+ spaceId: SpaceId,
410
+ body: QueryRequestProto,
411
+ args?: EdgeHttpCallArgs,
412
+ ): Promise<QueryResponseProto> {
413
+ return this._call(ctx, new URL(`/spaces/${spaceId}/exec-query`, this.baseUrl), { ...args, body, method: 'POST' });
355
414
  }
356
415
 
357
416
  //
@@ -359,19 +418,21 @@ export class EdgeHttpClient {
359
418
  //
360
419
 
361
420
  public async importBundle(
362
- spaceId: SpaceId, //
421
+ ctx: Context,
422
+ spaceId: SpaceId,
363
423
  body: ImportBundleRequest,
364
- args?: EdgeHttpGetArgs,
424
+ args?: EdgeHttpCallArgs,
365
425
  ): Promise<void> {
366
- return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
426
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
367
427
  }
368
428
 
369
429
  public async exportBundle(
430
+ ctx: Context,
370
431
  spaceId: SpaceId,
371
432
  body: ExportBundleRequest,
372
- args?: EdgeHttpGetArgs,
433
+ args?: EdgeHttpCallArgs,
373
434
  ): Promise<ExportBundleResponse> {
374
- return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
435
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
375
436
  ...args,
376
437
  body,
377
438
  method: 'POST',
@@ -390,16 +451,17 @@ export class EdgeHttpClient {
390
451
  Effect.provide(FetchHttpClient.layer),
391
452
  Effect.provide(HttpConfig.default),
392
453
  Effect.withSpan('EdgeHttpClient'),
393
- Effect.runPromise,
454
+ runAndForwardErrors,
394
455
  ) as T;
395
456
  }
396
457
 
397
458
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
398
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
459
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
399
460
  const shouldRetry = createRetryHandler(args);
400
- const requestContext = args.context ?? Context.default();
401
461
  log('fetch', { url, request: args.body });
402
462
 
463
+ const traceHeaders = getTraceHeaders(ctx);
464
+
403
465
  let handledAuth = false;
404
466
  const tryCount = 1;
405
467
  while (true) {
@@ -412,15 +474,12 @@ export class EdgeHttpClient {
412
474
  }
413
475
  }
414
476
 
415
- const request = createRequest(args, this._authHeader);
477
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
416
478
  log('call edge', { url, tryCount, authHeader: !!this._authHeader });
417
479
  const response = await fetch(url, request);
418
480
 
419
481
  if (response.ok) {
420
482
  const body = await response.clone().json();
421
- if (args.rawResponse) {
422
- return body as any;
423
- }
424
483
  invariant(body, 'Expected body to be present');
425
484
  if (!('success' in body)) {
426
485
  return body;
@@ -434,13 +493,13 @@ export class EdgeHttpClient {
434
493
  continue;
435
494
  }
436
495
 
437
- const body =
496
+ const body: EdgeFailure =
438
497
  response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
439
498
 
440
499
  invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
441
500
 
442
- if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
443
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
501
+ if (body?.data?.type === 'auth_challenge' && typeof body?.data?.challenge === 'string') {
502
+ processingError = new EdgeAuthChallengeError(body.data.challenge, body.data);
444
503
  } else if (body?.success === false) {
445
504
  processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
446
505
  } else {
@@ -451,7 +510,7 @@ export class EdgeHttpClient {
451
510
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
452
511
  }
453
512
 
454
- if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
513
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
455
514
  log.verbose('retrying edge request', { url, processingError });
456
515
  } else {
457
516
  throw processingError!;
@@ -473,6 +532,8 @@ export class EdgeHttpClient {
473
532
  const createRequest = (
474
533
  { method, body, json = true }: EdgeHttpRequestArgs,
475
534
  authHeader: string | undefined,
535
+ traceHeaders?: Record<string, string>,
536
+ clientTag?: string,
476
537
  ): RequestInit => {
477
538
  let requestBody: BodyInit | undefined;
478
539
  const headers: HeadersInit = {};
@@ -492,6 +553,14 @@ const createRequest = (
492
553
  headers['Authorization'] = authHeader;
493
554
  }
494
555
 
556
+ if (traceHeaders) {
557
+ Object.assign(headers, traceHeaders);
558
+ }
559
+
560
+ if (clientTag) {
561
+ headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
562
+ }
563
+
495
564
  return {
496
565
  method,
497
566
  body: requestBody,
@@ -499,6 +568,25 @@ const createRequest = (
499
568
  };
500
569
  };
501
570
 
571
+ /**
572
+ * Extract W3C Trace Context headers (traceparent/tracestate) from a DXOS Context.
573
+ */
574
+ const getTraceHeaders = (ctx: Context): Record<string, string> | undefined => {
575
+ const spanId = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE);
576
+ const otlpContext =
577
+ typeof spanId === 'number'
578
+ ? (TRACE_PROCESSOR.remoteTracing.getSpanContext(spanId) as OtelContext | undefined)
579
+ : undefined;
580
+
581
+ if (!otlpContext) {
582
+ return undefined;
583
+ }
584
+
585
+ const headers: Record<string, string> = {};
586
+ propagation.inject(otlpContext, headers);
587
+ return Object.keys(headers).length > 0 ? headers : undefined;
588
+ };
589
+
502
590
  /**
503
591
  * @deprecated
504
592
  */
@@ -50,7 +50,7 @@ export class EdgeWsConnection extends Resource {
50
50
 
51
51
  constructor(
52
52
  private readonly _identity: EdgeIdentity,
53
- private readonly _connectionInfo: { url: URL; protocolHeader?: string },
53
+ private readonly _connectionInfo: { url: URL; protocolHeader?: string; headers?: Record<string, string> },
54
54
  private readonly _callbacks: EdgeWsConnectionCallbacks,
55
55
  ) {
56
56
  super();
@@ -121,6 +121,7 @@ export class EdgeWsConnection extends Resource {
121
121
  this._connectionInfo.protocolHeader
122
122
  ? [...baseProtocols, this._connectionInfo.protocolHeader]
123
123
  : [...baseProtocols],
124
+ this._connectionInfo.headers ? { headers: this._connectionInfo.headers } : undefined,
124
125
  );
125
126
  const muxer = new WebSocketMuxer(this._ws);
126
127
  this._wsMuxer = muxer;
@@ -8,6 +8,7 @@ import * as Effect from 'effect/Effect';
8
8
  import * as Function from 'effect/Function';
9
9
  import { afterEach, beforeEach, describe, it } from 'vitest';
10
10
 
11
+ import { runAndForwardErrors } from '@dxos/effect';
11
12
  import { invariant } from '@dxos/invariant';
12
13
 
13
14
  import { HttpConfig, withLogging, withRetry, withRetryConfig } from './http-client';
@@ -36,7 +37,7 @@ describe('HttpClient', () => {
36
37
  withRetry(HttpClient.get(server.url)),
37
38
  Effect.provide(FetchHttpClient.layer),
38
39
  Effect.withSpan('EdgeHttpClient'),
39
- Effect.runPromise,
40
+ runAndForwardErrors,
40
41
  );
41
42
  expect(result).toMatchObject({ success: true, data: { value: 100 } });
42
43
  }
@@ -49,7 +50,7 @@ describe('HttpClient', () => {
49
50
  Effect.provide(FetchHttpClient.layer),
50
51
  Effect.provide(HttpConfig.default), // TODO(burdon): Swap out to mock.
51
52
  Effect.withSpan('EdgeHttpClient'), // TODO(burdon): OTEL.
52
- Effect.runPromise,
53
+ runAndForwardErrors,
53
54
  );
54
55
  expect(result).toMatchObject({ success: true, data: { value: 100 } });
55
56
  }
@@ -16,13 +16,13 @@ import { toUint8Array } from '../protocol';
16
16
 
17
17
  export const DEFAULT_PORT = 8080;
18
18
 
19
- type TestEdgeWsServerParams = {
19
+ type TestEdgeWsServerProps = {
20
20
  admitConnection?: Trigger;
21
21
  payloadDecoder?: (payload: Uint8Array) => any;
22
22
  messageHandler?: (payload: any) => Promise<Uint8Array | undefined>;
23
23
  };
24
24
 
25
- export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestEdgeWsServerParams) => {
25
+ export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestEdgeWsServerProps) => {
26
26
  const wsServer = new WebSocket.Server({
27
27
  port,
28
28
  verifyClient: createConnectionDelayHandler(params),
@@ -86,7 +86,7 @@ export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestE
86
86
  };
87
87
  };
88
88
 
89
- const createConnectionDelayHandler = (params: TestEdgeWsServerParams | undefined) => {
89
+ const createConnectionDelayHandler = (params: TestEdgeWsServerProps | undefined) => {
90
90
  return (_: any, callback: (admit: boolean) => void) => {
91
91
  if (params?.admitConnection) {
92
92
  log('delaying edge connection admission');
@@ -116,7 +116,7 @@ const createResponseSender = (connection: () => WebSocketMuxer) => {
116
116
  };
117
117
  };
118
118
 
119
- const decodePayload = async (request: Message, params: TestEdgeWsServerParams | undefined) => {
119
+ const decodePayload = async (request: Message, params: TestEdgeWsServerProps | undefined) => {
120
120
  const requestPayload = params?.payloadDecoder
121
121
  ? params.payloadDecoder(request.payload!.value!)
122
122
  : protocol.getPayload(request, TextMessageSchema);