@dxos/edge-client 0.8.3 → 0.8.4-main.84f28bd
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 → chunk-LMP5TVOP.mjs} +8 -8
- package/dist/lib/browser/edge-ws-muxer.mjs +1 -1
- package/dist/lib/browser/index.mjs +183 -128
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +61 -16
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/{chunk-HGQUUFIJ.mjs → chunk-X7J46ISZ.mjs} +8 -8
- package/dist/lib/node-esm/edge-ws-muxer.mjs +1 -1
- package/dist/lib/node-esm/index.mjs +183 -128
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +61 -16
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/edge-http-client.d.ts +32 -30
- 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/http-client.d.ts +22 -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/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/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -14
- package/src/edge-http-client.test.ts +22 -0
- package/src/edge-http-client.ts +188 -135
- package/src/http-client.test.ts +55 -0
- package/src/http-client.ts +67 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/test-server.ts +45 -0
- package/src/testing/test-utils.ts +1 -1
- 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/browser/{chunk-VHS3XEIX.mjs.map → chunk-LMP5TVOP.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-HGQUUFIJ.mjs.map → chunk-X7J46ISZ.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/edge-client",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-main.84f28bd",
|
|
4
4
|
"description": "EDGE Client",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -39,23 +39,27 @@
|
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@effect/platform": "^0.87.12",
|
|
42
43
|
"isomorphic-ws": "^5.0.0",
|
|
43
44
|
"ws": "^8.14.2",
|
|
44
|
-
"@dxos/async": "0.8.
|
|
45
|
-
"@dxos/context": "0.8.
|
|
46
|
-
"@dxos/credentials": "0.8.
|
|
47
|
-
"@dxos/debug": "0.8.
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/keyring": "0.8.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/node-std": "0.8.
|
|
54
|
-
"@dxos/protocols": "0.8.
|
|
55
|
-
"@dxos/util": "0.8.
|
|
45
|
+
"@dxos/async": "0.8.4-main.84f28bd",
|
|
46
|
+
"@dxos/context": "0.8.4-main.84f28bd",
|
|
47
|
+
"@dxos/credentials": "0.8.4-main.84f28bd",
|
|
48
|
+
"@dxos/debug": "0.8.4-main.84f28bd",
|
|
49
|
+
"@dxos/crypto": "0.8.4-main.84f28bd",
|
|
50
|
+
"@dxos/invariant": "0.8.4-main.84f28bd",
|
|
51
|
+
"@dxos/keyring": "0.8.4-main.84f28bd",
|
|
52
|
+
"@dxos/keys": "0.8.4-main.84f28bd",
|
|
53
|
+
"@dxos/log": "0.8.4-main.84f28bd",
|
|
54
|
+
"@dxos/node-std": "0.8.4-main.84f28bd",
|
|
55
|
+
"@dxos/protocols": "0.8.4-main.84f28bd",
|
|
56
|
+
"@dxos/util": "0.8.4-main.84f28bd"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@dxos/test-utils": "0.8.
|
|
59
|
+
"@dxos/test-utils": "0.8.4-main.84f28bd"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"effect": "^3.13.3"
|
|
59
63
|
},
|
|
60
64
|
"publishConfig": {
|
|
61
65
|
"access": "public"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createEphemeralEdgeIdentity } from './auth';
|
|
8
|
+
import { EdgeHttpClient } from './edge-http-client';
|
|
9
|
+
|
|
10
|
+
// TODO(burdon): Factor out config.
|
|
11
|
+
const DEV_SERVER = 'https://edge.dxos.workers.dev';
|
|
12
|
+
|
|
13
|
+
describe.skipIf(process.env.CI)('EdgeHttpClient', () => {
|
|
14
|
+
it.only('should get status', async ({ expect }) => {
|
|
15
|
+
const client = new EdgeHttpClient(DEV_SERVER);
|
|
16
|
+
const identity = await createEphemeralEdgeIdentity();
|
|
17
|
+
client.setIdentity(identity);
|
|
18
|
+
|
|
19
|
+
const result = await client.getStatus();
|
|
20
|
+
expect(result).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/edge-http-client.ts
CHANGED
|
@@ -2,42 +2,73 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { FetchHttpClient, HttpClient } from '@effect/platform';
|
|
6
|
+
import { Effect, pipe } from 'effect';
|
|
7
|
+
|
|
5
8
|
import { sleep } from '@dxos/async';
|
|
6
9
|
import { Context } from '@dxos/context';
|
|
7
10
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
8
11
|
import { log } from '@dxos/log';
|
|
9
12
|
import {
|
|
13
|
+
type CreateAgentResponseBody,
|
|
14
|
+
type CreateAgentRequestBody,
|
|
15
|
+
type CreateSpaceRequest,
|
|
16
|
+
type CreateSpaceResponseBody,
|
|
17
|
+
EdgeAuthChallengeError,
|
|
10
18
|
EdgeCallFailedError,
|
|
11
19
|
type EdgeHttpResponse,
|
|
20
|
+
type ExecuteWorkflowResponseBody,
|
|
21
|
+
type GetAgentStatusResponseBody,
|
|
12
22
|
type GetNotarizationResponseBody,
|
|
13
|
-
type
|
|
23
|
+
type InitiateOAuthFlowRequest,
|
|
24
|
+
type InitiateOAuthFlowResponse,
|
|
14
25
|
type JoinSpaceRequest,
|
|
15
26
|
type JoinSpaceResponseBody,
|
|
16
|
-
EdgeAuthChallengeError,
|
|
17
|
-
type CreateAgentResponseBody,
|
|
18
|
-
type CreateAgentRequestBody,
|
|
19
|
-
type GetAgentStatusResponseBody,
|
|
20
27
|
type RecoverIdentityRequest,
|
|
21
28
|
type RecoverIdentityResponseBody,
|
|
22
|
-
type UploadFunctionRequest,
|
|
23
|
-
type UploadFunctionResponseBody,
|
|
24
29
|
type ObjectId,
|
|
25
|
-
type
|
|
30
|
+
type PostNotarizationRequestBody,
|
|
26
31
|
type QueueQuery,
|
|
27
32
|
type QueryResult,
|
|
28
|
-
type
|
|
29
|
-
type
|
|
30
|
-
type
|
|
31
|
-
type CreateSpaceResponseBody,
|
|
33
|
+
type UploadFunctionRequest,
|
|
34
|
+
type UploadFunctionResponseBody,
|
|
35
|
+
type EdgeStatus,
|
|
32
36
|
} from '@dxos/protocols';
|
|
37
|
+
import { createUrl } from '@dxos/util';
|
|
33
38
|
|
|
34
39
|
import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
|
|
40
|
+
import { encodeAuthHeader, HttpConfig, withLogging, withRetryConfig } from './http-client';
|
|
35
41
|
import { getEdgeUrlWithProtocol } from './utils';
|
|
36
42
|
|
|
37
43
|
const DEFAULT_RETRY_TIMEOUT = 1500;
|
|
38
44
|
const DEFAULT_RETRY_JITTER = 500;
|
|
39
45
|
const DEFAULT_MAX_RETRIES_COUNT = 3;
|
|
40
46
|
|
|
47
|
+
export type RetryConfig = {
|
|
48
|
+
/**
|
|
49
|
+
* A number of call retries, not counting the initial request.
|
|
50
|
+
*/
|
|
51
|
+
count: number;
|
|
52
|
+
/**
|
|
53
|
+
* Delay before retries in ms.
|
|
54
|
+
*/
|
|
55
|
+
timeout?: number;
|
|
56
|
+
/**
|
|
57
|
+
* A random amount of time before retrying to help prevent large bursts of requests.
|
|
58
|
+
*/
|
|
59
|
+
jitter?: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type EdgeHttpRequestArgs = {
|
|
63
|
+
method: string;
|
|
64
|
+
context?: Context;
|
|
65
|
+
retry?: RetryConfig;
|
|
66
|
+
body?: any;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
|
|
70
|
+
export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
|
|
71
|
+
|
|
41
72
|
export class EdgeHttpClient {
|
|
42
73
|
private readonly _baseUrl: string;
|
|
43
74
|
|
|
@@ -64,19 +95,38 @@ export class EdgeHttpClient {
|
|
|
64
95
|
}
|
|
65
96
|
}
|
|
66
97
|
|
|
98
|
+
//
|
|
99
|
+
// Status
|
|
100
|
+
//
|
|
101
|
+
|
|
102
|
+
public async getStatus(args?: EdgeHttpGetArgs): Promise<EdgeStatus> {
|
|
103
|
+
return this._call(new URL('/status', this.baseUrl), { ...args, method: 'GET' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//
|
|
107
|
+
// Agents
|
|
108
|
+
//
|
|
109
|
+
|
|
67
110
|
public createAgent(body: CreateAgentRequestBody, args?: EdgeHttpGetArgs): Promise<CreateAgentResponseBody> {
|
|
68
|
-
return this._call('/agents/create', { ...args, method: 'POST', body });
|
|
111
|
+
return this._call(new URL('/agents/create', this.baseUrl), { ...args, method: 'POST', body });
|
|
69
112
|
}
|
|
70
113
|
|
|
71
114
|
public getAgentStatus(
|
|
72
115
|
request: { ownerIdentityKey: PublicKey },
|
|
73
116
|
args?: EdgeHttpGetArgs,
|
|
74
117
|
): Promise<GetAgentStatusResponseBody> {
|
|
75
|
-
return this._call(`/users/${request.ownerIdentityKey.toHex()}/agent/status`,
|
|
118
|
+
return this._call(new URL(`/users/${request.ownerIdentityKey.toHex()}/agent/status`, this.baseUrl), {
|
|
119
|
+
...args,
|
|
120
|
+
method: 'GET',
|
|
121
|
+
});
|
|
76
122
|
}
|
|
77
123
|
|
|
124
|
+
//
|
|
125
|
+
// Credentials
|
|
126
|
+
//
|
|
127
|
+
|
|
78
128
|
public getCredentialsForNotarization(spaceId: SpaceId, args?: EdgeHttpGetArgs): Promise<GetNotarizationResponseBody> {
|
|
79
|
-
return this._call(`/spaces/${spaceId}/notarization`, { ...args, method: 'GET' });
|
|
129
|
+
return this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, method: 'GET' });
|
|
80
130
|
}
|
|
81
131
|
|
|
82
132
|
public async notarizeCredentials(
|
|
@@ -84,49 +134,55 @@ export class EdgeHttpClient {
|
|
|
84
134
|
body: PostNotarizationRequestBody,
|
|
85
135
|
args?: EdgeHttpGetArgs,
|
|
86
136
|
): Promise<void> {
|
|
87
|
-
await this._call(`/spaces/${spaceId}/notarization`, { ...args, body, method: 'POST' });
|
|
137
|
+
await this._call(new URL(`/spaces/${spaceId}/notarization`, this.baseUrl), { ...args, body, method: 'POST' });
|
|
88
138
|
}
|
|
89
139
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
args?: EdgeHttpGetArgs,
|
|
94
|
-
): Promise<JoinSpaceResponseBody> {
|
|
95
|
-
return this._call(`/spaces/${spaceId}/join`, { ...args, body, method: 'POST' });
|
|
96
|
-
}
|
|
140
|
+
//
|
|
141
|
+
// Identity
|
|
142
|
+
//
|
|
97
143
|
|
|
98
144
|
public async recoverIdentity(
|
|
99
145
|
body: RecoverIdentityRequest,
|
|
100
146
|
args?: EdgeHttpGetArgs,
|
|
101
147
|
): Promise<RecoverIdentityResponseBody> {
|
|
102
|
-
return this._call('/identity/recover', { ...args, body, method: 'POST' });
|
|
148
|
+
return this._call(new URL('/identity/recover', this.baseUrl), { ...args, body, method: 'POST' });
|
|
103
149
|
}
|
|
104
150
|
|
|
105
|
-
|
|
151
|
+
//
|
|
152
|
+
// Invitations
|
|
153
|
+
//
|
|
154
|
+
|
|
155
|
+
public async joinSpaceByInvitation(
|
|
106
156
|
spaceId: SpaceId,
|
|
107
|
-
|
|
108
|
-
input: any,
|
|
157
|
+
body: JoinSpaceRequest,
|
|
109
158
|
args?: EdgeHttpGetArgs,
|
|
110
|
-
): Promise<
|
|
111
|
-
return this._call(`/
|
|
159
|
+
): Promise<JoinSpaceResponseBody> {
|
|
160
|
+
return this._call(new URL(`/spaces/${spaceId}/join`, this.baseUrl), { ...args, body, method: 'POST' });
|
|
112
161
|
}
|
|
113
162
|
|
|
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
|
-
}
|
|
163
|
+
//
|
|
164
|
+
// OAuth and credentials
|
|
165
|
+
//
|
|
122
166
|
|
|
123
167
|
public async initiateOAuthFlow(
|
|
124
168
|
body: InitiateOAuthFlowRequest,
|
|
125
169
|
args?: EdgeHttpGetArgs,
|
|
126
170
|
): Promise<InitiateOAuthFlowResponse> {
|
|
127
|
-
return this._call('/oauth/initiate', { ...args, body, method: 'POST' });
|
|
171
|
+
return this._call(new URL('/oauth/initiate', this.baseUrl), { ...args, body, method: 'POST' });
|
|
128
172
|
}
|
|
129
173
|
|
|
174
|
+
//
|
|
175
|
+
// Spaces
|
|
176
|
+
//
|
|
177
|
+
|
|
178
|
+
async createSpace(body: CreateSpaceRequest, args?: EdgeHttpGetArgs): Promise<CreateSpaceResponseBody> {
|
|
179
|
+
return this._call(new URL('/spaces/create', this.baseUrl), { ...args, body, method: 'POST' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
//
|
|
183
|
+
// Queues
|
|
184
|
+
//
|
|
185
|
+
|
|
130
186
|
public async queryQueue(
|
|
131
187
|
subspaceTag: string,
|
|
132
188
|
spaceId: SpaceId,
|
|
@@ -134,26 +190,19 @@ export class EdgeHttpClient {
|
|
|
134
190
|
args?: EdgeHttpGetArgs,
|
|
135
191
|
): Promise<QueryResult> {
|
|
136
192
|
const { queueId } = query;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
});
|
|
193
|
+
return this._call(
|
|
194
|
+
createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}/query`, this.baseUrl), {
|
|
195
|
+
after: query.after,
|
|
196
|
+
before: query.before,
|
|
197
|
+
limit: query.limit,
|
|
198
|
+
reverse: query.reverse,
|
|
199
|
+
objectIds: query.objectIds?.join(','),
|
|
200
|
+
}),
|
|
201
|
+
{
|
|
202
|
+
...args,
|
|
203
|
+
method: 'GET',
|
|
204
|
+
},
|
|
205
|
+
);
|
|
157
206
|
}
|
|
158
207
|
|
|
159
208
|
public async insertIntoQueue(
|
|
@@ -163,72 +212,105 @@ export class EdgeHttpClient {
|
|
|
163
212
|
objects: unknown[],
|
|
164
213
|
args?: EdgeHttpGetArgs,
|
|
165
214
|
): Promise<void> {
|
|
166
|
-
return this._call(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, {
|
|
215
|
+
return this._call(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
|
|
167
216
|
...args,
|
|
168
217
|
body: { objects },
|
|
169
218
|
method: 'POST',
|
|
170
219
|
});
|
|
171
220
|
}
|
|
172
221
|
|
|
173
|
-
async deleteFromQueue(
|
|
222
|
+
public async deleteFromQueue(
|
|
174
223
|
subspaceTag: string,
|
|
175
224
|
spaceId: SpaceId,
|
|
176
225
|
queueId: ObjectId,
|
|
177
226
|
objectIds: ObjectId[],
|
|
178
227
|
args?: EdgeHttpGetArgs,
|
|
179
228
|
): Promise<void> {
|
|
180
|
-
return this._call(
|
|
229
|
+
return this._call(
|
|
230
|
+
createUrl(new URL(`/spaces/${subspaceTag}/${spaceId}/queue/${queueId}`, this.baseUrl), {
|
|
231
|
+
ids: objectIds.join(','),
|
|
232
|
+
}),
|
|
233
|
+
{
|
|
234
|
+
...args,
|
|
235
|
+
method: 'DELETE',
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
//
|
|
241
|
+
// Functions
|
|
242
|
+
//
|
|
243
|
+
|
|
244
|
+
public async uploadFunction(
|
|
245
|
+
pathParts: { functionId?: string },
|
|
246
|
+
body: UploadFunctionRequest,
|
|
247
|
+
args?: EdgeHttpGetArgs,
|
|
248
|
+
): Promise<UploadFunctionResponseBody> {
|
|
249
|
+
const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
|
|
250
|
+
return this._call(new URL(path, this.baseUrl), { ...args, body, method: 'PUT' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//
|
|
254
|
+
// Workflows
|
|
255
|
+
//
|
|
256
|
+
|
|
257
|
+
public async executeWorkflow(
|
|
258
|
+
spaceId: SpaceId,
|
|
259
|
+
graphId: ObjectId,
|
|
260
|
+
input: any,
|
|
261
|
+
args?: EdgeHttpGetArgs,
|
|
262
|
+
): Promise<ExecuteWorkflowResponseBody> {
|
|
263
|
+
return this._call(new URL(`/workflows/${spaceId}/${graphId}`, this.baseUrl), {
|
|
181
264
|
...args,
|
|
182
|
-
|
|
183
|
-
method: '
|
|
265
|
+
body: input,
|
|
266
|
+
method: 'POST',
|
|
184
267
|
});
|
|
185
268
|
}
|
|
186
269
|
|
|
187
|
-
|
|
188
|
-
|
|
270
|
+
//
|
|
271
|
+
// Internal
|
|
272
|
+
//
|
|
273
|
+
|
|
274
|
+
private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
|
|
275
|
+
return pipe(
|
|
276
|
+
HttpClient.get(url),
|
|
277
|
+
withLogging,
|
|
278
|
+
withRetryConfig,
|
|
279
|
+
Effect.provide(FetchHttpClient.layer),
|
|
280
|
+
Effect.provide(HttpConfig.default),
|
|
281
|
+
Effect.withSpan('EdgeHttpClient'),
|
|
282
|
+
Effect.runPromise,
|
|
283
|
+
) as T;
|
|
189
284
|
}
|
|
190
285
|
|
|
191
|
-
|
|
192
|
-
|
|
286
|
+
// TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
|
|
287
|
+
private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
|
|
193
288
|
const shouldRetry = createRetryHandler(args);
|
|
194
|
-
|
|
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()}`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
log('call', { method: args.method, path, request: args.body });
|
|
289
|
+
const requestContext = args.context ?? new Context();
|
|
290
|
+
log('fetch', { url, request: args.body });
|
|
205
291
|
|
|
206
292
|
let handledAuth = false;
|
|
207
|
-
let authHeader = this._authHeader;
|
|
208
293
|
while (true) {
|
|
209
294
|
let processingError: EdgeCallFailedError;
|
|
210
295
|
let retryAfterHeaderValue: number = Number.NaN;
|
|
211
296
|
try {
|
|
212
|
-
const request = createRequest(args,
|
|
297
|
+
const request = createRequest(args, this._authHeader);
|
|
213
298
|
const response = await fetch(url, request);
|
|
214
|
-
|
|
215
299
|
retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
|
|
216
|
-
|
|
217
300
|
if (response.ok) {
|
|
218
301
|
const body = (await response.json()) as EdgeHttpResponse<T>;
|
|
219
302
|
if (body.success) {
|
|
220
303
|
return body.data;
|
|
221
304
|
}
|
|
222
305
|
|
|
223
|
-
log('unsuccessful edge response', {
|
|
224
|
-
|
|
306
|
+
log.warn('unsuccessful edge response', { url, body });
|
|
225
307
|
if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
|
|
226
308
|
processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
|
|
227
309
|
} else {
|
|
228
310
|
processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
|
|
229
311
|
}
|
|
230
312
|
} else if (response.status === 401 && !handledAuth) {
|
|
231
|
-
|
|
313
|
+
this._authHeader = await this._handleUnauthorized(response);
|
|
232
314
|
handledAuth = true;
|
|
233
315
|
continue;
|
|
234
316
|
} else {
|
|
@@ -239,7 +321,7 @@ export class EdgeHttpClient {
|
|
|
239
321
|
}
|
|
240
322
|
|
|
241
323
|
if (processingError.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
|
|
242
|
-
log('retrying edge request', {
|
|
324
|
+
log('retrying edge request', { url, processingError });
|
|
243
325
|
} else {
|
|
244
326
|
throw processingError;
|
|
245
327
|
}
|
|
@@ -248,24 +330,35 @@ export class EdgeHttpClient {
|
|
|
248
330
|
|
|
249
331
|
private async _handleUnauthorized(response: Response): Promise<string> {
|
|
250
332
|
if (!this._edgeIdentity) {
|
|
251
|
-
log.warn('
|
|
333
|
+
log.warn('unauthorized response received before identity was set');
|
|
252
334
|
throw EdgeCallFailedError.fromHttpFailure(response);
|
|
253
335
|
}
|
|
336
|
+
|
|
254
337
|
const challenge = await handleAuthChallenge(response, this._edgeIdentity);
|
|
255
|
-
|
|
256
|
-
log('auth header updated');
|
|
257
|
-
return this._authHeader;
|
|
338
|
+
return encodeAuthHeader(challenge);
|
|
258
339
|
}
|
|
259
340
|
}
|
|
260
341
|
|
|
261
|
-
const
|
|
262
|
-
|
|
342
|
+
const createRequest = ({ method, body }: EdgeHttpRequestArgs, authHeader: string | undefined): RequestInit => {
|
|
343
|
+
return {
|
|
344
|
+
method,
|
|
345
|
+
body: body && JSON.stringify(body),
|
|
346
|
+
headers: authHeader ? { Authorization: authHeader } : undefined,
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @deprecated
|
|
352
|
+
*/
|
|
353
|
+
const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
|
|
354
|
+
if (!retry || retry.count < 1) {
|
|
263
355
|
return async () => false;
|
|
264
356
|
}
|
|
357
|
+
|
|
265
358
|
let retries = 0;
|
|
266
|
-
const maxRetries =
|
|
267
|
-
const baseTimeout =
|
|
268
|
-
const jitter =
|
|
359
|
+
const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
|
|
360
|
+
const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
|
|
361
|
+
const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
|
|
269
362
|
return async (ctx: Context, retryAfter: number) => {
|
|
270
363
|
if (++retries > maxRetries || ctx.disposed) {
|
|
271
364
|
return false;
|
|
@@ -281,43 +374,3 @@ const createRetryHandler = (args: EdgeHttpCallArgs) => {
|
|
|
281
374
|
return true;
|
|
282
375
|
};
|
|
283
376
|
};
|
|
284
|
-
|
|
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
|
-
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { FetchHttpClient, HttpClient } from '@effect/platform';
|
|
6
|
+
import { Effect, pipe } from 'effect';
|
|
7
|
+
import { afterEach, beforeEach, describe, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
|
|
11
|
+
import { HttpConfig, withLogging, withRetry, withRetryConfig } from './http-client';
|
|
12
|
+
import { type TestServer, createTestServer, responseHandler } from './testing';
|
|
13
|
+
|
|
14
|
+
describe('HttpClient', () => {
|
|
15
|
+
let server: TestServer | undefined;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
server = await createTestServer(responseHandler((attempt) => (attempt > 2 ? { value: 100 } : false)));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line mocha/no-top-level-hooks
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
server?.close();
|
|
24
|
+
server = undefined;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Auth headers.
|
|
28
|
+
// TODO(burdon): Add request/response schema type checking.
|
|
29
|
+
it.skipIf(process.env.CI)('should retry', async ({ expect }) => {
|
|
30
|
+
invariant(server);
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
const result = await pipe(
|
|
34
|
+
withRetry(HttpClient.get(server.url)),
|
|
35
|
+
Effect.provide(FetchHttpClient.layer),
|
|
36
|
+
Effect.withSpan('EdgeHttpClient'),
|
|
37
|
+
Effect.runPromise,
|
|
38
|
+
);
|
|
39
|
+
expect(result).toMatchObject({ success: true, data: { value: 100 } });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
const result = await pipe(
|
|
44
|
+
HttpClient.get(server.url),
|
|
45
|
+
withLogging,
|
|
46
|
+
withRetryConfig,
|
|
47
|
+
Effect.provide(FetchHttpClient.layer),
|
|
48
|
+
Effect.provide(HttpConfig.default), // TODO(burdon): Swap out to mock.
|
|
49
|
+
Effect.withSpan('EdgeHttpClient'), // TODO(burdon): OTEL.
|
|
50
|
+
Effect.runPromise,
|
|
51
|
+
);
|
|
52
|
+
expect(result).toMatchObject({ success: true, data: { value: 100 } });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type HttpClient } from '@effect/platform';
|
|
6
|
+
import { type HttpClientError } from '@effect/platform/HttpClientError';
|
|
7
|
+
import { type HttpClientResponse } from '@effect/platform/HttpClientResponse';
|
|
8
|
+
import { Context, Duration, Effect, Layer, Schedule } from 'effect';
|
|
9
|
+
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Factor out.
|
|
13
|
+
|
|
14
|
+
export type RetryOptions = {
|
|
15
|
+
timeout: Duration.Duration;
|
|
16
|
+
retryTimes: number;
|
|
17
|
+
retryBaseDelay: Duration.Duration;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Layer pattern.
|
|
21
|
+
export class HttpConfig extends Context.Tag('HttpConfig')<HttpConfig, RetryOptions>() {
|
|
22
|
+
static default = Layer.succeed(HttpConfig, {
|
|
23
|
+
timeout: Duration.millis(1_000),
|
|
24
|
+
retryTimes: 3,
|
|
25
|
+
retryBaseDelay: Duration.millis(1_000),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// HOC pattern.
|
|
30
|
+
export const withRetry = (
|
|
31
|
+
effect: Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>,
|
|
32
|
+
{
|
|
33
|
+
timeout = Duration.millis(1_000),
|
|
34
|
+
retryBaseDelay = Duration.millis(1_000),
|
|
35
|
+
retryTimes = 3,
|
|
36
|
+
}: Partial<RetryOptions> = {},
|
|
37
|
+
) => {
|
|
38
|
+
return effect.pipe(
|
|
39
|
+
Effect.flatMap((res) =>
|
|
40
|
+
// Treat 500 errors as retryable?
|
|
41
|
+
res.status === 500 ? Effect.fail(new Error(res.status.toString())) : res.json,
|
|
42
|
+
),
|
|
43
|
+
Effect.timeout(timeout),
|
|
44
|
+
Effect.retry({
|
|
45
|
+
schedule: Schedule.exponential(retryBaseDelay).pipe(Schedule.jittered),
|
|
46
|
+
times: retryTimes,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const withRetryConfig = (effect: Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>) =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const config = yield* HttpConfig;
|
|
54
|
+
return yield* withRetry(effect, config);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const withLogging = <A extends HttpClientResponse, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
58
|
+
effect.pipe(Effect.tap((res) => log.info('response', { status: res.status })));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
*
|
|
62
|
+
*/
|
|
63
|
+
// TODO(burdon): Document.
|
|
64
|
+
export const encodeAuthHeader = (challenge: Uint8Array) => {
|
|
65
|
+
const encodedChallenge = Buffer.from(challenge).toString('base64');
|
|
66
|
+
return `VerifiablePresentation pb;base64,${encodedChallenge}`;
|
|
67
|
+
};
|