@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.
- package/dist/lib/{browser/chunk-VHS3XEIX.mjs → neutral/chunk-ZIQ5T3A7.mjs} +20 -50
- package/dist/lib/{browser/chunk-VHS3XEIX.mjs.map → neutral/chunk-ZIQ5T3A7.mjs.map} +3 -3
- package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
- package/dist/lib/neutral/index.mjs +1189 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +53 -33
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/auth.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +18 -15
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +98 -37
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.test.d.ts +2 -0
- package/dist/types/src/edge-http-client.test.d.ts.map +1 -0
- package/dist/types/src/edge-identity.d.ts.map +1 -1
- package/dist/types/src/edge-ws-connection.d.ts +21 -0
- package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
- package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/http-client.d.ts +25 -0
- package/dist/types/src/http-client.d.ts.map +1 -0
- package/dist/types/src/http-client.test.d.ts +2 -0
- package/dist/types/src/http-client.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/protocol.d.ts +1 -1
- package/dist/types/src/protocol.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-server.d.ts +9 -0
- package/dist/types/src/testing/test-server.d.ts.map +1 -0
- package/dist/types/src/testing/test-utils.d.ts +3 -3
- package/dist/types/src/testing/test-utils.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +1 -1
- package/dist/types/src/utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -29
- package/src/edge-client.test.ts +20 -15
- package/src/edge-client.ts +90 -43
- package/src/edge-http-client.test.ts +23 -0
- package/src/edge-http-client.ts +502 -164
- package/src/edge-ws-connection.ts +131 -9
- package/src/edge-ws-muxer.ts +1 -1
- package/src/http-client.test.ts +58 -0
- package/src/http-client.ts +77 -0
- package/src/index.ts +4 -3
- package/src/testing/index.ts +1 -0
- package/src/testing/test-server.ts +45 -0
- package/src/testing/test-utils.ts +9 -9
- package/src/websocket.test.ts +1 -1
- package/dist/lib/browser/index.mjs +0 -1034
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/chunk-XNHBUTNB.cjs +0 -317
- package/dist/lib/node/chunk-XNHBUTNB.cjs.map +0 -7
- package/dist/lib/node/edge-ws-muxer.cjs +0 -33
- package/dist/lib/node/edge-ws-muxer.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -1060
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -169
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-HGQUUFIJ.mjs +0 -299
- package/dist/lib/node-esm/chunk-HGQUUFIJ.mjs.map +0 -7
- package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
- package/dist/lib/node-esm/edge-ws-muxer.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1035
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -141
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- /package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs.map +0 -0
package/src/edge-http-client.ts
CHANGED
|
@@ -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
|
|
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
|
|
34
|
+
type ImportBundleRequest,
|
|
35
|
+
type InitiateOAuthFlowRequest,
|
|
36
|
+
type InitiateOAuthFlowResponse,
|
|
14
37
|
type JoinSpaceRequest,
|
|
15
38
|
type JoinSpaceResponseBody,
|
|
16
|
-
|
|
17
|
-
type
|
|
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
|
-
|
|
68
|
-
|
|
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?:
|
|
169
|
+
args?: EdgeHttpCallArgs,
|
|
74
170
|
): Promise<GetAgentStatusResponseBody> {
|
|
75
|
-
return this._call(`/users/${request.ownerIdentityKey.toHex()}/agent/status`,
|
|
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
|
-
|
|
79
|
-
|
|
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?:
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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?:
|
|
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
|
-
|
|
210
|
+
//
|
|
211
|
+
// Invitations
|
|
212
|
+
//
|
|
213
|
+
|
|
214
|
+
public async joinSpaceByInvitation(
|
|
215
|
+
ctx: Context,
|
|
106
216
|
spaceId: SpaceId,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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?:
|
|
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?:
|
|
135
|
-
): Promise<
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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?:
|
|
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?:
|
|
293
|
+
args?: EdgeHttpCallArgs,
|
|
179
294
|
): Promise<void> {
|
|
180
|
-
return this._call(
|
|
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
|
-
|
|
183
|
-
method: '
|
|
334
|
+
body: formData,
|
|
335
|
+
method: 'PUT',
|
|
336
|
+
json: false,
|
|
184
337
|
});
|
|
185
338
|
}
|
|
186
339
|
|
|
187
|
-
async
|
|
188
|
-
return this._call('/
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
242
|
-
log('retrying edge request', {
|
|
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('
|
|
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
|
-
|
|
256
|
-
log('auth header updated');
|
|
257
|
-
return this._authHeader;
|
|
569
|
+
return encodeAuthHeader(challenge);
|
|
258
570
|
}
|
|
259
571
|
}
|
|
260
572
|
|
|
261
|
-
const
|
|
262
|
-
|
|
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 =
|
|
267
|
-
const baseTimeout =
|
|
268
|
-
const jitter =
|
|
269
|
-
return async (ctx: Context, retryAfter
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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';
|