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

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 (56) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{browser/chunk-LMP5TVOP.mjs → neutral/chunk-ZIQ5T3A7.mjs} +13 -43
  3. package/dist/lib/{browser/chunk-LMP5TVOP.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +3 -3
  4. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  5. package/dist/lib/{browser → neutral}/index.mjs +576 -489
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/lib/{browser → neutral}/testing/index.mjs +6 -31
  9. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  10. package/dist/types/src/auth.d.ts.map +1 -1
  11. package/dist/types/src/edge-client.d.ts +18 -15
  12. package/dist/types/src/edge-client.d.ts.map +1 -1
  13. package/dist/types/src/edge-http-client.d.ts +80 -22
  14. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  15. package/dist/types/src/edge-identity.d.ts.map +1 -1
  16. package/dist/types/src/edge-ws-connection.d.ts +20 -0
  17. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  18. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  19. package/dist/types/src/errors.d.ts.map +1 -1
  20. package/dist/types/src/http-client.d.ts +10 -7
  21. package/dist/types/src/http-client.d.ts.map +1 -1
  22. package/dist/types/src/index.d.ts +4 -3
  23. package/dist/types/src/index.d.ts.map +1 -1
  24. package/dist/types/src/protocol.d.ts +1 -1
  25. package/dist/types/src/protocol.d.ts.map +1 -1
  26. package/dist/types/src/testing/test-server.d.ts.map +1 -1
  27. package/dist/types/src/testing/test-utils.d.ts +3 -3
  28. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  29. package/dist/types/src/utils.d.ts +1 -1
  30. package/dist/types/src/utils.d.ts.map +1 -1
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +32 -32
  33. package/src/edge-client.test.ts +20 -15
  34. package/src/edge-client.ts +90 -43
  35. package/src/edge-http-client.test.ts +3 -2
  36. package/src/edge-http-client.ts +355 -74
  37. package/src/edge-ws-connection.ts +121 -7
  38. package/src/edge-ws-muxer.ts +1 -1
  39. package/src/http-client.test.ts +11 -7
  40. package/src/http-client.ts +18 -8
  41. package/src/index.ts +4 -3
  42. package/src/testing/test-utils.ts +8 -8
  43. package/src/websocket.test.ts +1 -1
  44. package/dist/lib/browser/index.mjs.map +0 -7
  45. package/dist/lib/browser/meta.json +0 -1
  46. package/dist/lib/browser/testing/index.mjs.map +0 -7
  47. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs +0 -299
  48. package/dist/lib/node-esm/chunk-X7J46ISZ.mjs.map +0 -7
  49. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  50. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  51. package/dist/lib/node-esm/index.mjs +0 -1103
  52. package/dist/lib/node-esm/index.mjs.map +0 -7
  53. package/dist/lib/node-esm/meta.json +0 -1
  54. package/dist/lib/node-esm/testing/index.mjs +0 -186
  55. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  56. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,47 +2,72 @@
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 GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
22
33
  type GetNotarizationResponseBody,
34
+ type ImportBundleRequest,
23
35
  type InitiateOAuthFlowRequest,
24
36
  type InitiateOAuthFlowResponse,
25
37
  type JoinSpaceRequest,
26
38
  type JoinSpaceResponseBody,
27
- type RecoverIdentityRequest,
28
- type RecoverIdentityResponseBody,
29
39
  type ObjectId,
30
40
  type PostNotarizationRequestBody,
31
- type QueueQuery,
32
- type QueryResult,
41
+ type RecoverIdentityRequest,
42
+ type RecoverIdentityResponseBody,
33
43
  type UploadFunctionRequest,
34
44
  type UploadFunctionResponseBody,
35
- type EdgeStatus,
36
45
  } from '@dxos/protocols';
46
+ import {
47
+ type QueryRequest as QueryRequestProto,
48
+ type QueryResponse as QueryResponseProto,
49
+ } from '@dxos/protocols/proto/dxos/echo/query';
37
50
  import { createUrl } from '@dxos/util';
38
51
 
39
52
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
40
- import { encodeAuthHeader, HttpConfig, withLogging, withRetryConfig } from './http-client';
53
+ import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
41
54
  import { getEdgeUrlWithProtocol } from './utils';
42
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
+
43
67
  const DEFAULT_RETRY_TIMEOUT = 1500;
44
68
  const DEFAULT_RETRY_JITTER = 500;
45
69
  const DEFAULT_MAX_RETRIES_COUNT = 3;
70
+ const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
46
71
 
47
72
  export type RetryConfig = {
48
73
  /**
@@ -61,16 +86,38 @@ export type RetryConfig = {
61
86
 
62
87
  type EdgeHttpRequestArgs = {
63
88
  method: string;
64
- context?: Context;
65
89
  retry?: RetryConfig;
66
90
  body?: any;
91
+ /**
92
+ * @default true
93
+ */
94
+ json?: boolean;
95
+
96
+ /**
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.
100
+ */
101
+ auth?: boolean;
67
102
  };
68
103
 
69
- export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
70
- 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
+ };
71
117
 
72
118
  export class EdgeHttpClient {
73
119
  private readonly _baseUrl: string;
120
+ private readonly _clientTag: string | undefined;
74
121
 
75
122
  private _edgeIdentity: EdgeIdentity | undefined;
76
123
 
@@ -79,8 +126,9 @@ export class EdgeHttpClient {
79
126
  */
80
127
  private _authHeader: string | undefined;
81
128
 
82
- constructor(baseUrl: string) {
129
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
83
130
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
131
+ this._clientTag = options?.clientTag;
84
132
  log('created', { url: this._baseUrl });
85
133
  }
86
134
 
@@ -99,23 +147,28 @@ export class EdgeHttpClient {
99
147
  // Status
100
148
  //
101
149
 
102
- public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
103
- 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 });
104
152
  }
105
153
 
106
154
  //
107
155
  // Agents
108
156
  //
109
157
 
110
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
111
- 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 });
112
164
  }
113
165
 
114
166
  public getAgentStatus(
167
+ ctx: Context,
115
168
  request: { ownerIdentityKey: PublicKey },
116
- args?: EdgeHttpGetArgs,
169
+ args?: EdgeHttpCallArgs,
117
170
  ): Promise<GetAgentStatusResponseBody> {
118
- 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), {
119
172
  ...args,
120
173
  method: 'GET',
121
174
  });
@@ -125,16 +178,21 @@ export class EdgeHttpClient {
125
178
  // Credentials
126
179
  //
127
180
 
128
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
129
- 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' });
130
187
  }
131
188
 
132
189
  public async notarizeCredentials(
190
+ ctx: Context,
133
191
  spaceId: SpaceId,
134
192
  body: PostNotarizationRequestBody,
135
- args?: EdgeHttpGetArgs,
193
+ args?: EdgeHttpCallArgs,
136
194
  ): Promise<void> {
137
- 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' });
138
196
  }
139
197
 
140
198
  //
@@ -142,10 +200,11 @@ export class EdgeHttpClient {
142
200
  //
143
201
 
144
202
  public async recoverIdentity(
203
+ ctx: Context,
145
204
  body: RecoverIdentityRequest,
146
- args?: EdgeHttpGetArgs,
205
+ args?: EdgeHttpCallArgs,
147
206
  ): Promise<RecoverIdentityResponseBody> {
148
- 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' });
149
208
  }
150
209
 
151
210
  //
@@ -153,34 +212,32 @@ export class EdgeHttpClient {
153
212
  //
154
213
 
155
214
  public async joinSpaceByInvitation(
215
+ ctx: Context,
156
216
  spaceId: SpaceId,
157
217
  body: JoinSpaceRequest,
158
- args?: EdgeHttpGetArgs,
218
+ args?: EdgeHttpCallArgs,
159
219
  ): Promise<JoinSpaceResponseBody> {
160
- 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' });
161
221
  }
162
222
 
163
223
  //
164
224
  // OAuth and credentials
165
225
  //
166
226
 
167
- public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
168
- return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
169
- }
170
-
171
227
  public async initiateOAuthFlow(
228
+ ctx: Context,
172
229
  body: InitiateOAuthFlowRequest,
173
- args?: EdgeHttpGetArgs,
230
+ args?: EdgeHttpCallArgs,
174
231
  ): Promise<InitiateOAuthFlowResponse> {
175
- 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' });
176
233
  }
177
234
 
178
235
  //
179
236
  // Spaces
180
237
  //
181
238
 
182
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
183
- 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' });
184
241
  }
185
242
 
186
243
  //
@@ -188,13 +245,16 @@ export class EdgeHttpClient {
188
245
  //
189
246
 
190
247
  public async queryQueue(
248
+ ctx: Context,
191
249
  subspaceTag: string,
192
250
  spaceId: SpaceId,
193
- query: QueueQuery,
194
- args?: EdgeHttpGetArgs,
195
- ): Promise<QueryResult> {
196
- const { queueId } = query;
251
+ query: FeedProtocol.QueueQuery,
252
+ args?: EdgeHttpCallArgs,
253
+ ): Promise<EdgeQueryQueueResponse> {
254
+ const queueId = query.queueIds?.[0];
255
+ invariant(queueId, 'queueId required');
197
256
  return this._call(
257
+ ctx,
198
258
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
199
259
  after: query.after,
200
260
  before: query.before,
@@ -210,13 +270,14 @@ export class EdgeHttpClient {
210
270
  }
211
271
 
212
272
  public async insertIntoQueue(
273
+ ctx: Context,
213
274
  subspaceTag: string,
214
275
  spaceId: SpaceId,
215
276
  queueId: ObjectId,
216
277
  objects: unknown[],
217
- args?: EdgeHttpGetArgs,
278
+ args?: EdgeHttpCallArgs,
218
279
  ): Promise<void> {
219
- 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), {
220
281
  ...args,
221
282
  body: { objects },
222
283
  method: 'POST',
@@ -224,13 +285,15 @@ export class EdgeHttpClient {
224
285
  }
225
286
 
226
287
  public async deleteFromQueue(
288
+ ctx: Context,
227
289
  subspaceTag: string,
228
290
  spaceId: SpaceId,
229
291
  queueId: ObjectId,
230
292
  objectIds: ObjectId[],
231
- args?: EdgeHttpGetArgs,
293
+ args?: EdgeHttpCallArgs,
232
294
  ): Promise<void> {
233
295
  return this._call(
296
+ ctx,
234
297
  createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
235
298
  ids: objectIds.join(','),
236
299
  }),
@@ -246,12 +309,69 @@ export class EdgeHttpClient {
246
309
  //
247
310
 
248
311
  public async uploadFunction(
312
+ ctx: Context,
249
313
  pathParts: { functionId?: string },
250
314
  body: UploadFunctionRequest,
251
- args?: EdgeHttpGetArgs,
315
+ args?: EdgeHttpCallArgs,
252
316
  ): Promise<UploadFunctionResponseBody> {
317
+ const formData = new FormData();
318
+ formData.append('name', body.name ?? '');
319
+ formData.append('version', body.version);
320
+ formData.append('ownerPublicKey', body.ownerPublicKey);
321
+ formData.append('entryPoint', body.entryPoint);
322
+ body.runtime && formData.append('runtime', body.runtime);
323
+ for (const [filename, content] of Object.entries(body.assets)) {
324
+ formData.append(
325
+ 'assets',
326
+ new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
327
+ filename,
328
+ );
329
+ }
330
+
253
331
  const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
254
- return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
332
+ return this._call(ctx, new URL(path, this.baseUrl), {
333
+ ...args,
334
+ body: formData,
335
+ method: 'PUT',
336
+ json: false,
337
+ });
338
+ }
339
+
340
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
341
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
342
+ }
343
+
344
+ public async invokeFunction(
345
+ ctx: Context,
346
+ params: {
347
+ functionId: string;
348
+ version?: string;
349
+ spaceId?: SpaceId;
350
+ cpuTimeLimit?: number;
351
+ subrequestsLimit?: number;
352
+ },
353
+ input: unknown,
354
+ args?: EdgeHttpCallArgs,
355
+ ): Promise<any> {
356
+ const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
357
+ if (params.version) {
358
+ url.searchParams.set('version', params.version);
359
+ }
360
+ if (params.spaceId) {
361
+ url.searchParams.set('spaceId', params.spaceId.toString());
362
+ }
363
+ if (params.cpuTimeLimit) {
364
+ url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
365
+ }
366
+ if (params.subrequestsLimit) {
367
+ url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
368
+ }
369
+
370
+ return this._call(ctx, url, {
371
+ ...args,
372
+ body: input,
373
+ method: 'POST',
374
+ });
255
375
  }
256
376
 
257
377
  //
@@ -259,75 +379,182 @@ export class EdgeHttpClient {
259
379
  //
260
380
 
261
381
  public async executeWorkflow(
382
+ ctx: Context,
262
383
  spaceId: SpaceId,
263
384
  graphId: ObjectId,
264
385
  input: any,
265
- args?: EdgeHttpGetArgs,
386
+ args?: EdgeHttpCallArgs,
266
387
  ): Promise<ExecuteWorkflowResponseBody> {
267
- return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
388
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
268
389
  ...args,
269
390
  body: input,
270
391
  method: 'POST',
271
392
  });
272
393
  }
273
394
 
395
+ //
396
+ // Triggers
397
+ //
398
+
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
+ });
455
+ }
456
+
457
+ //
458
+ // Import/Export space.
459
+ //
460
+
461
+ public async importBundle(
462
+ ctx: Context,
463
+ spaceId: SpaceId,
464
+ body: ImportBundleRequest,
465
+ args?: EdgeHttpCallArgs,
466
+ ): Promise<void> {
467
+ return this._call(ctx, new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
468
+ }
469
+
470
+ public async exportBundle(
471
+ ctx: Context,
472
+ spaceId: SpaceId,
473
+ body: ExportBundleRequest,
474
+ args?: EdgeHttpCallArgs,
475
+ ): Promise<ExportBundleResponse> {
476
+ return this._call(ctx, new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
477
+ ...args,
478
+ body,
479
+ method: 'POST',
480
+ });
481
+ }
482
+
274
483
  //
275
484
  // Internal
276
485
  //
277
486
 
278
- private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
279
- return pipe(
487
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
488
+ return Function.pipe(
280
489
  HttpClient.get(url),
281
490
  withLogging,
282
491
  withRetryConfig,
283
492
  Effect.provide(FetchHttpClient.layer),
284
493
  Effect.provide(HttpConfig.default),
285
494
  Effect.withSpan('EdgeHttpClient'),
286
- Effect.runPromise,
495
+ runAndForwardErrors,
287
496
  ) as T;
288
497
  }
289
498
 
290
499
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
291
- private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
500
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
292
501
  const shouldRetry = createRetryHandler(args);
293
- const requestContext = args.context ?? new Context();
294
502
  log('fetch', { url, request: args.body });
295
503
 
504
+ const traceHeaders = getTraceHeaders(ctx);
505
+
296
506
  let handledAuth = false;
507
+ const tryCount = 1;
297
508
  while (true) {
298
- let processingError: EdgeCallFailedError;
299
- let retryAfterHeaderValue: number = Number.NaN;
509
+ let processingError: EdgeCallFailedError | undefined = undefined;
300
510
  try {
301
- const request = createRequest(args, this._authHeader);
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);
515
+ }
516
+ }
517
+
518
+ const request = createRequest(args, this._authHeader, traceHeaders, this._clientTag);
519
+ log('call edge', { url, tryCount, authHeader: !!this._authHeader });
302
520
  const response = await fetch(url, request);
303
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
521
+
304
522
  if (response.ok) {
305
- const body = (await response.json()) as EdgeHttpResponse<T>;
523
+ const body = await response.clone().json();
524
+ invariant(body, 'Expected body to be present');
525
+ if (!('success' in body)) {
526
+ return body;
527
+ }
306
528
  if (body.success) {
307
529
  return body.data;
308
530
  }
309
-
310
- log.warn('unsuccessful edge response', { url, body });
311
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
312
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
313
- } else {
314
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
315
- }
316
531
  } else if (response.status === 401 && !handledAuth) {
317
532
  this._authHeader = await this._handleUnauthorized(response);
318
533
  handledAuth = true;
319
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);
320
546
  } else {
321
- processingError = EdgeCallFailedError.fromHttpFailure(response);
547
+ invariant(!response.ok, 'Expected response to not be ok.');
548
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
322
549
  }
323
550
  } catch (error: any) {
324
551
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
325
552
  }
326
553
 
327
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
328
- log('retrying edge request', { url, processingError });
554
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
555
+ log.verbose('retrying edge request', { url, processingError });
329
556
  } else {
330
- throw processingError;
557
+ throw processingError!;
331
558
  }
332
559
  }
333
560
  }
@@ -335,7 +562,7 @@ export class EdgeHttpClient {
335
562
  private async _handleUnauthorized(response: Response): Promise<string> {
336
563
  if (!this._edgeIdentity) {
337
564
  log.warn('unauthorized response received before identity was set');
338
- throw EdgeCallFailedError.fromHttpFailure(response);
565
+ throw await EdgeCallFailedError.fromHttpFailure(response);
339
566
  }
340
567
 
341
568
  const challenge = await handleAuthChallenge(response, this._edgeIdentity);
@@ -343,14 +570,61 @@ export class EdgeHttpClient {
343
570
  }
344
571
  }
345
572
 
346
- const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
573
+ const createRequest = (
574
+ { method, body, json = true }: EdgeHttpRequestArgs,
575
+ authHeader: string | undefined,
576
+ traceHeaders?: Record<string, string>,
577
+ clientTag?: string,
578
+ ): RequestInit => {
579
+ let requestBody: BodyInit | undefined;
580
+ const headers: HeadersInit = {};
581
+
582
+ if (json) {
583
+ requestBody = body && JSON.stringify(body);
584
+ headers['Content-Type'] = 'application/json';
585
+ } else {
586
+ requestBody = body;
587
+ }
588
+
589
+ if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
590
+ log.warn('Request with large body', { bodySize: requestBody.length });
591
+ }
592
+
593
+ if (authHeader) {
594
+ headers['Authorization'] = authHeader;
595
+ }
596
+
597
+ if (traceHeaders) {
598
+ Object.assign(headers, traceHeaders);
599
+ }
600
+
601
+ if (clientTag) {
602
+ headers[EDGE_CLIENT_TAG_HEADER] = clientTag;
603
+ }
604
+
347
605
  return {
348
606
  method,
349
- body: body && JSON.stringify(body),
350
- headers: authHeader ? { Authorization: authHeader } : undefined,
607
+ body: requestBody,
608
+ headers,
351
609
  };
352
610
  };
353
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
+
354
628
  /**
355
629
  * @deprecated
356
630
  */
@@ -363,7 +637,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
363
637
  const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
364
638
  const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
365
639
  const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
366
- return async (ctx: Context, retryAfter: number) => {
640
+ return async (ctx: Context, retryAfter?: number) => {
367
641
  if (++retries > maxRetries || ctx.disposed) {
368
642
  return false;
369
643
  }
@@ -378,3 +652,10 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
378
652
  return true;
379
653
  };
380
654
  };
655
+
656
+ const getFileMimeType = (filename: string) =>
657
+ ['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
658
+ ? 'application/javascript+module'
659
+ : filename.endsWith('.wasm')
660
+ ? 'application/wasm'
661
+ : 'application/octet-stream';