@dxos/edge-client 0.8.3 → 0.8.4-main.1c7ec43d41

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 (74) hide show
  1. package/dist/lib/{browser/chunk-VHS3XEIX.mjs → neutral/chunk-ZIQ5T3A7.mjs} +20 -50
  2. package/dist/lib/{browser/chunk-VHS3XEIX.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +3 -3
  3. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  4. package/dist/lib/neutral/index.mjs +1189 -0
  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 +53 -33
  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 +18 -15
  11. package/dist/types/src/edge-client.d.ts.map +1 -1
  12. package/dist/types/src/edge-http-client.d.ts +98 -37
  13. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  14. package/dist/types/src/edge-http-client.test.d.ts +2 -0
  15. package/dist/types/src/edge-http-client.test.d.ts.map +1 -0
  16. package/dist/types/src/edge-identity.d.ts.map +1 -1
  17. package/dist/types/src/edge-ws-connection.d.ts +21 -0
  18. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  19. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  20. package/dist/types/src/errors.d.ts.map +1 -1
  21. package/dist/types/src/http-client.d.ts +25 -0
  22. package/dist/types/src/http-client.d.ts.map +1 -0
  23. package/dist/types/src/http-client.test.d.ts +2 -0
  24. package/dist/types/src/http-client.test.d.ts.map +1 -0
  25. package/dist/types/src/index.d.ts +4 -3
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/protocol.d.ts +1 -1
  28. package/dist/types/src/protocol.d.ts.map +1 -1
  29. package/dist/types/src/testing/index.d.ts +1 -0
  30. package/dist/types/src/testing/index.d.ts.map +1 -1
  31. package/dist/types/src/testing/test-server.d.ts +9 -0
  32. package/dist/types/src/testing/test-server.d.ts.map +1 -0
  33. package/dist/types/src/testing/test-utils.d.ts +3 -3
  34. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  35. package/dist/types/src/utils.d.ts +1 -1
  36. package/dist/types/src/utils.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +33 -29
  39. package/src/edge-client.test.ts +20 -15
  40. package/src/edge-client.ts +90 -43
  41. package/src/edge-http-client.test.ts +23 -0
  42. package/src/edge-http-client.ts +502 -164
  43. package/src/edge-ws-connection.ts +131 -9
  44. package/src/edge-ws-muxer.ts +1 -1
  45. package/src/http-client.test.ts +58 -0
  46. package/src/http-client.ts +77 -0
  47. package/src/index.ts +4 -3
  48. package/src/testing/index.ts +1 -0
  49. package/src/testing/test-server.ts +45 -0
  50. package/src/testing/test-utils.ts +9 -9
  51. package/src/websocket.test.ts +1 -1
  52. package/dist/lib/browser/index.mjs +0 -1034
  53. package/dist/lib/browser/index.mjs.map +0 -7
  54. package/dist/lib/browser/meta.json +0 -1
  55. package/dist/lib/browser/testing/index.mjs.map +0 -7
  56. package/dist/lib/node/chunk-XNHBUTNB.cjs +0 -317
  57. package/dist/lib/node/chunk-XNHBUTNB.cjs.map +0 -7
  58. package/dist/lib/node/edge-ws-muxer.cjs +0 -33
  59. package/dist/lib/node/edge-ws-muxer.cjs.map +0 -7
  60. package/dist/lib/node/index.cjs +0 -1060
  61. package/dist/lib/node/index.cjs.map +0 -7
  62. package/dist/lib/node/meta.json +0 -1
  63. package/dist/lib/node/testing/index.cjs +0 -169
  64. package/dist/lib/node/testing/index.cjs.map +0 -7
  65. package/dist/lib/node-esm/chunk-HGQUUFIJ.mjs +0 -299
  66. package/dist/lib/node-esm/chunk-HGQUUFIJ.mjs.map +0 -7
  67. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  68. package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
  69. package/dist/lib/node-esm/index.mjs +0 -1035
  70. package/dist/lib/node-esm/index.mjs.map +0 -7
  71. package/dist/lib/node-esm/meta.json +0 -1
  72. package/dist/lib/node-esm/testing/index.mjs +0 -141
  73. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  74. /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,44 +2,122 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
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';
9
+
5
10
  import { sleep } from '@dxos/async';
6
- 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';
7
14
  import { type PublicKey, type SpaceId } from '@dxos/keys';
8
15
  import { log } from '@dxos/log';
9
16
  import {
17
+ type CreateAgentRequestBody,
18
+ type CreateAgentResponseBody,
19
+ type CreateSpaceRequest,
20
+ type CreateSpaceResponseBody,
21
+ EDGE_CLIENT_TAG_HEADER,
22
+ EdgeAuthChallengeError,
10
23
  EdgeCallFailedError,
11
- type EdgeHttpResponse,
24
+ type EdgeFailure,
25
+ type EdgeStatus,
26
+ type ExecuteWorkflowResponseBody,
27
+ type ExportBundleRequest,
28
+ type ExportBundleResponse,
29
+ type FeedProtocol,
30
+ type GetAgentStatusResponseBody,
31
+ type GetPluginVersionsResponseBody,
32
+ type GetPluginsResponseBody,
12
33
  type GetNotarizationResponseBody,
13
- type PostNotarizationRequestBody,
34
+ type ImportBundleRequest,
35
+ type InitiateOAuthFlowRequest,
36
+ type InitiateOAuthFlowResponse,
14
37
  type JoinSpaceRequest,
15
38
  type JoinSpaceResponseBody,
16
- EdgeAuthChallengeError,
17
- type CreateAgentResponseBody,
18
- type CreateAgentRequestBody,
19
- type GetAgentStatusResponseBody,
39
+ type ObjectId,
40
+ type PostNotarizationRequestBody,
20
41
  type RecoverIdentityRequest,
21
42
  type RecoverIdentityResponseBody,
22
43
  type UploadFunctionRequest,
23
44
  type UploadFunctionResponseBody,
24
- type ObjectId,
25
- type ExecuteWorkflowResponseBody,
26
- type QueueQuery,
27
- type QueryResult,
28
- type InitiateOAuthFlowRequest,
29
- type InitiateOAuthFlowResponse,
30
- type CreateSpaceRequest,
31
- type CreateSpaceResponseBody,
32
45
  } from '@dxos/protocols';
46
+ import {
47
+ type QueryRequest as QueryRequestProto,
48
+ type QueryResponse as QueryResponseProto,
49
+ } from '@dxos/protocols/proto/dxos/echo/query';
50
+ import { createUrl } from '@dxos/util';
33
51
 
34
52
  import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
53
+ import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
35
54
  import { getEdgeUrlWithProtocol } from './utils';
36
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
+
37
67
  const DEFAULT_RETRY_TIMEOUT = 1500;
38
68
  const DEFAULT_RETRY_JITTER = 500;
39
69
  const DEFAULT_MAX_RETRIES_COUNT = 3;
70
+ const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
71
+
72
+ export type RetryConfig = {
73
+ /**
74
+ * A number of call retries, not counting the initial request.
75
+ */
76
+ count: number;
77
+ /**
78
+ * Delay before retries in ms.
79
+ */
80
+ timeout?: number;
81
+ /**
82
+ * A random amount of time before retrying to help prevent large bursts of requests.
83
+ */
84
+ jitter?: number;
85
+ };
86
+
87
+ type EdgeHttpRequestArgs = {
88
+ method: string;
89
+ retry?: RetryConfig;
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;
102
+ };
103
+
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
+ };
40
117
 
41
118
  export class EdgeHttpClient {
42
119
  private readonly _baseUrl: string;
120
+ private readonly _clientTag: string | undefined;
43
121
 
44
122
  private _edgeIdentity: EdgeIdentity | undefined;
45
123
 
@@ -48,8 +126,9 @@ export class EdgeHttpClient {
48
126
  */
49
127
  private _authHeader: string | undefined;
50
128
 
51
- constructor(baseUrl: string) {
129
+ constructor(baseUrl: string, options?: EdgeHttpClientOptions) {
52
130
  this._baseUrl = getEdgeUrlWithProtocol(baseUrl, 'http');
131
+ this._clientTag = options?.clientTag;
53
132
  log('created', { url: this._baseUrl });
54
133
  }
55
134
 
@@ -64,209 +143,501 @@ export class EdgeHttpClient {
64
143
  }
65
144
  }
66
145
 
67
- public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
68
- return this._call('/agents/create', { ...args, method: 'POST', body });
146
+ //
147
+ // Status
148
+ //
149
+
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 });
152
+ }
153
+
154
+ //
155
+ // Agents
156
+ //
157
+
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 });
69
164
  }
70
165
 
71
166
  public getAgentStatus(
167
+ ctx: Context,
72
168
  request: { ownerIdentityKey: PublicKey },
73
- args?: EdgeHttpGetArgs,
169
+ args?: EdgeHttpCallArgs,
74
170
  ): Promise<GetAgentStatusResponseBody> {
75
- return this._call(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, { ...args, method: 'GET' });
171
+ return this._call(ctx, new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
172
+ ...args,
173
+ method: 'GET',
174
+ });
76
175
  }
77
176
 
78
- public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
79
- return this._call(`/spaces/${spaceId}/notarization`, { ...args, method: 'GET' });
177
+ //
178
+ // Credentials
179
+ //
180
+
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' });
80
187
  }
81
188
 
82
189
  public async notarizeCredentials(
190
+ ctx: Context,
83
191
  spaceId: SpaceId,
84
192
  body: PostNotarizationRequestBody,
85
- args?: EdgeHttpGetArgs,
193
+ args?: EdgeHttpCallArgs,
86
194
  ): Promise<void> {
87
- await this._call(`/spaces/${spaceId}/notarization`, { ...args, body, method: 'POST' });
195
+ await this._call(ctx, new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
88
196
  }
89
197
 
90
- public async joinSpaceByInvitation(
91
- spaceId: SpaceId,
92
- body: JoinSpaceRequest,
93
- args?: EdgeHttpGetArgs,
94
- ): Promise<JoinSpaceResponseBody> {
95
- return this._call(`/spaces/${spaceId}/join`, { ...args, body, method: 'POST' });
96
- }
198
+ //
199
+ // Identity
200
+ //
97
201
 
98
202
  public async recoverIdentity(
203
+ ctx: Context,
99
204
  body: RecoverIdentityRequest,
100
- args?: EdgeHttpGetArgs,
205
+ args?: EdgeHttpCallArgs,
101
206
  ): Promise<RecoverIdentityResponseBody> {
102
- return this._call('/identity/recover', { ...args, body, method: 'POST' });
207
+ return this._call(ctx, new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
103
208
  }
104
209
 
105
- public async executeWorkflow(
210
+ //
211
+ // Invitations
212
+ //
213
+
214
+ public async joinSpaceByInvitation(
215
+ ctx: Context,
106
216
  spaceId: SpaceId,
107
- graphId: ObjectId,
108
- input: any,
109
- args?: EdgeHttpGetArgs,
110
- ): Promise<ExecuteWorkflowResponseBody> {
111
- return this._call(`/workflows/${spaceId}/${graphId}`, { ...args, body: input, method: 'POST' });
217
+ body: JoinSpaceRequest,
218
+ args?: EdgeHttpCallArgs,
219
+ ): Promise<JoinSpaceResponseBody> {
220
+ return this._call(ctx, new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
112
221
  }
113
222
 
114
- public async uploadFunction(
115
- pathParts: { functionId?: string },
116
- body: UploadFunctionRequest,
117
- args?: EdgeHttpGetArgs,
118
- ): Promise<UploadFunctionResponseBody> {
119
- const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
120
- return this._call(path, { ...args, body, method: 'PUT' });
121
- }
223
+ //
224
+ // OAuth and credentials
225
+ //
122
226
 
123
227
  public async initiateOAuthFlow(
228
+ ctx: Context,
124
229
  body: InitiateOAuthFlowRequest,
125
- args?: EdgeHttpGetArgs,
230
+ args?: EdgeHttpCallArgs,
126
231
  ): Promise<InitiateOAuthFlowResponse> {
127
- return this._call('/oauth/initiate', { ...args, body, method: 'POST' });
232
+ return this._call(ctx, new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
233
+ }
234
+
235
+ //
236
+ // Spaces
237
+ //
238
+
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' });
128
241
  }
129
242
 
243
+ //
244
+ // Queues
245
+ //
246
+
130
247
  public async queryQueue(
248
+ ctx: Context,
131
249
  subspaceTag: string,
132
250
  spaceId: SpaceId,
133
- query: QueueQuery,
134
- args?: EdgeHttpGetArgs,
135
- ): Promise<QueryResult> {
136
- const { queueId } = query;
137
- const queryParams = new URLSearchParams();
138
- if (query.after != null) {
139
- queryParams.set('after', query.after);
140
- }
141
- if (query.before != null) {
142
- queryParams.set('before', query.before);
143
- }
144
- if (query.limit != null) {
145
- queryParams.set('limit', query.limit.toString());
146
- }
147
- if (query.reverse != null) {
148
- queryParams.set('reverse', query.reverse.toString());
149
- }
150
- if (query.objectIds != null) {
151
- queryParams.set('objectIds', query.objectIds.join(','));
152
- }
153
- return this._call(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query?${queryParams.toString()}`, {
154
- ...args,
155
- method: 'GET',
156
- });
251
+ query: FeedProtocol.QueueQuery,
252
+ args?: EdgeHttpCallArgs,
253
+ ): Promise<EdgeQueryQueueResponse> {
254
+ const queueId = query.queueIds?.[0];
255
+ invariant(queueId, 'queueId required');
256
+ return this._call(
257
+ ctx,
258
+ createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
259
+ after: query.after,
260
+ before: query.before,
261
+ limit: query.limit,
262
+ reverse: query.reverse,
263
+ objectIds: query.objectIds?.join(','),
264
+ }),
265
+ {
266
+ ...args,
267
+ method: 'GET',
268
+ },
269
+ );
157
270
  }
158
271
 
159
272
  public async insertIntoQueue(
273
+ ctx: Context,
160
274
  subspaceTag: string,
161
275
  spaceId: SpaceId,
162
276
  queueId: ObjectId,
163
277
  objects: unknown[],
164
- args?: EdgeHttpGetArgs,
278
+ args?: EdgeHttpCallArgs,
165
279
  ): Promise<void> {
166
- return this._call(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, {
280
+ return this._call(ctx, new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
167
281
  ...args,
168
282
  body: { objects },
169
283
  method: 'POST',
170
284
  });
171
285
  }
172
286
 
173
- async deleteFromQueue(
287
+ public async deleteFromQueue(
288
+ ctx: Context,
174
289
  subspaceTag: string,
175
290
  spaceId: SpaceId,
176
291
  queueId: ObjectId,
177
292
  objectIds: ObjectId[],
178
- args?: EdgeHttpGetArgs,
293
+ args?: EdgeHttpCallArgs,
179
294
  ): Promise<void> {
180
- return this._call(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, {
295
+ return this._call(
296
+ ctx,
297
+ createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
298
+ ids: objectIds.join(','),
299
+ }),
300
+ {
301
+ ...args,
302
+ method: 'DELETE',
303
+ },
304
+ );
305
+ }
306
+
307
+ //
308
+ // Functions
309
+ //
310
+
311
+ public async uploadFunction(
312
+ ctx: Context,
313
+ pathParts: { functionId?: string },
314
+ body: UploadFunctionRequest,
315
+ args?: EdgeHttpCallArgs,
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
+
331
+ const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
332
+ return this._call(ctx, new URL(path, this.baseUrl), {
181
333
  ...args,
182
- query: { ids: objectIds.join(',') },
183
- method: 'DELETE',
334
+ body: formData,
335
+ method: 'PUT',
336
+ json: false,
184
337
  });
185
338
  }
186
339
 
187
- async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
188
- return this._call('/spaces/create', { ...args, body, method: 'POST' });
340
+ public async listFunctions(ctx: Context, args?: EdgeHttpCallArgs): Promise<any> {
341
+ return this._call(ctx, new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
189
342
  }
190
343
 
191
- private async _call<T>(path: string, args: EdgeHttpCallArgs): Promise<T> {
192
- const requestContext = args.context ?? new Context();
193
- const shouldRetry = createRetryHandler(args);
194
- let url = `${this._baseUrl}${path.startsWith('/') ? path.slice(1) : path}`;
195
-
196
- if (args.query) {
197
- const queryParams = new URLSearchParams();
198
- for (const [key, value] of Object.entries(args.query)) {
199
- queryParams.set(key, value.toString());
200
- }
201
- url += `?${queryParams.toString()}`;
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());
202
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
+ });
375
+ }
376
+
377
+ //
378
+ // Workflows
379
+ //
380
+
381
+ public async executeWorkflow(
382
+ ctx: Context,
383
+ spaceId: SpaceId,
384
+ graphId: ObjectId,
385
+ input: any,
386
+ args?: EdgeHttpCallArgs,
387
+ ): Promise<ExecuteWorkflowResponseBody> {
388
+ return this._call(ctx, new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
389
+ ...args,
390
+ body: input,
391
+ method: 'POST',
392
+ });
393
+ }
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
+ }
203
426
 
204
- log('call', { method: args.method, path, request: args.body });
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
+
483
+ //
484
+ // Internal
485
+ //
486
+
487
+ private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
488
+ return Function.pipe(
489
+ HttpClient.get(url),
490
+ withLogging,
491
+ withRetryConfig,
492
+ Effect.provide(FetchHttpClient.layer),
493
+ Effect.provide(HttpConfig.default),
494
+ Effect.withSpan('EdgeHttpClient'),
495
+ runAndForwardErrors,
496
+ ) as T;
497
+ }
498
+
499
+ // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
500
+ private async _call<T>(ctx: Context, url: URL, args: EdgeHttpRequestArgs): Promise<T> {
501
+ const shouldRetry = createRetryHandler(args);
502
+ log('fetch', { url, request: args.body });
503
+
504
+ const traceHeaders = getTraceHeaders(ctx);
205
505
 
206
506
  let handledAuth = false;
207
- let authHeader = this._authHeader;
507
+ const tryCount = 1;
208
508
  while (true) {
209
- let processingError: EdgeCallFailedError;
210
- let retryAfterHeaderValue: number = Number.NaN;
509
+ let processingError: EdgeCallFailedError | undefined = undefined;
211
510
  try {
212
- const request = createRequest(args, authHeader);
213
- const response = await fetch(url, request);
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
+ }
214
517
 
215
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
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);
216
521
 
217
522
  if (response.ok) {
218
- 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
+ }
219
528
  if (body.success) {
220
529
  return body.data;
221
530
  }
222
-
223
- log('unsuccessful edge response', { path, body });
224
-
225
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
226
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
227
- } else {
228
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
229
- }
230
531
  } else if (response.status === 401 && !handledAuth) {
231
- authHeader = await this._handleUnauthorized(response);
532
+ this._authHeader = await this._handleUnauthorized(response);
232
533
  handledAuth = true;
233
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);
234
546
  } else {
235
- processingError = EdgeCallFailedError.fromHttpFailure(response);
547
+ invariant(!response.ok, 'Expected response to not be ok.');
548
+ processingError = await EdgeCallFailedError.fromHttpFailure(response);
236
549
  }
237
550
  } catch (error: any) {
238
551
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
239
552
  }
240
553
 
241
- if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
242
- log('retrying edge request', { path, processingError });
554
+ if (processingError?.isRetryable && (await shouldRetry(ctx, processingError.retryAfterMs))) {
555
+ log.verbose('retrying edge request', { url, processingError });
243
556
  } else {
244
- throw processingError;
557
+ throw processingError!;
245
558
  }
246
559
  }
247
560
  }
248
561
 
249
562
  private async _handleUnauthorized(response: Response): Promise<string> {
250
563
  if (!this._edgeIdentity) {
251
- log.warn('edge unauthorized response received before identity was set');
252
- throw EdgeCallFailedError.fromHttpFailure(response);
564
+ log.warn('unauthorized response received before identity was set');
565
+ throw await EdgeCallFailedError.fromHttpFailure(response);
253
566
  }
567
+
254
568
  const challenge = await handleAuthChallenge(response, this._edgeIdentity);
255
- this._authHeader = encodeAuthHeader(challenge);
256
- log('auth header updated');
257
- return this._authHeader;
569
+ return encodeAuthHeader(challenge);
258
570
  }
259
571
  }
260
572
 
261
- const createRetryHandler = (args: EdgeHttpCallArgs) => {
262
- if (!args.retry || args.retry.count < 1) {
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
+
605
+ return {
606
+ method,
607
+ body: requestBody,
608
+ headers,
609
+ };
610
+ };
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
+
628
+ /**
629
+ * @deprecated
630
+ */
631
+ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
632
+ if (!retry || retry.count < 1) {
263
633
  return async () => false;
264
634
  }
635
+
265
636
  let retries = 0;
266
- const maxRetries = args.retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
267
- const baseTimeout = args.retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
268
- const jitter = args.retry.jitter ?? DEFAULT_RETRY_JITTER;
269
- return async (ctx: Context, retryAfter: number) => {
637
+ const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
638
+ const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
639
+ const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
640
+ return async (ctx: Context, retryAfter?: number) => {
270
641
  if (++retries > maxRetries || ctx.disposed) {
271
642
  return false;
272
643
  }
@@ -282,42 +653,9 @@ const createRetryHandler = (args: EdgeHttpCallArgs) => {
282
653
  };
283
654
  };
284
655
 
285
- export type RetryConfig = {
286
- /**
287
- * A number of call retries, not counting the initial request.
288
- */
289
- count: number;
290
- /**
291
- * Delay before retries in ms.
292
- */
293
- timeout?: number;
294
- /**
295
- * A random amount of time before retrying to help prevent large bursts of requests.
296
- */
297
- jitter?: number;
298
- };
299
-
300
- export type EdgeHttpGetArgs = { context?: Context; retry?: RetryConfig };
301
-
302
- export type EdgeHttpPostArgs = { context?: Context; body?: any; retry?: RetryConfig };
303
-
304
- type EdgeHttpCallArgs = {
305
- method: string;
306
- body?: any;
307
- context?: Context;
308
- retry?: RetryConfig;
309
- query?: Record<string, string>;
310
- };
311
-
312
- const createRequest = (args: EdgeHttpCallArgs, authHeader: string | undefined): RequestInit => {
313
- return {
314
- method: args.method,
315
- body: args.body && JSON.stringify(args.body),
316
- headers: authHeader ? { Authorization: authHeader } : undefined,
317
- };
318
- };
319
-
320
- const encodeAuthHeader = (challenge: Uint8Array) => {
321
- const encodedChallenge = Buffer.from(challenge).toString('base64');
322
- return `VerifiablePresentation pb;base64,${encodedChallenge}`;
323
- };
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';