@dxos/edge-client 0.8.4-main.f9ba587 → 0.8.4-main.fffef41
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-LMP5TVOP.mjs → chunk-VESGVCLQ.mjs} +8 -4
- package/dist/lib/browser/{chunk-LMP5TVOP.mjs.map → chunk-VESGVCLQ.mjs.map} +3 -3
- package/dist/lib/browser/edge-ws-muxer.mjs +1 -1
- package/dist/lib/browser/index.mjs +557 -284
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +1 -1
- package/dist/lib/browser/testing/index.mjs.map +2 -2
- package/dist/lib/node-esm/{chunk-X7J46ISZ.mjs → chunk-JTBFRYNM.mjs} +8 -4
- package/dist/lib/node-esm/{chunk-X7J46ISZ.mjs.map → chunk-JTBFRYNM.mjs.map} +3 -3
- package/dist/lib/node-esm/edge-ws-muxer.mjs +1 -1
- package/dist/lib/node-esm/index.mjs +557 -284
- 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 +1 -1
- package/dist/lib/node-esm/testing/index.mjs.map +2 -2
- package/dist/types/src/edge-client.d.ts +15 -15
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +30 -3
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-ws-connection.d.ts +20 -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/http-client.d.ts +10 -7
- package/dist/types/src/http-client.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-utils.d.ts +1 -1
- package/dist/types/src/testing/test-utils.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -15
- package/src/edge-client.test.ts +4 -4
- package/src/edge-client.ts +73 -42
- package/src/edge-http-client.test.ts +1 -1
- package/src/edge-http-client.ts +194 -35
- package/src/edge-ws-connection.ts +129 -8
- package/src/edge-ws-muxer.ts +1 -1
- package/src/http-client.test.ts +8 -6
- package/src/http-client.ts +18 -8
- package/src/index.ts +4 -3
- package/src/testing/test-utils.ts +4 -4
- package/src/websocket.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/edge-client",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.fffef41",
|
|
4
4
|
"description": "EDGE Client",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
13
14
|
"types": "./dist/types/src/index.d.ts",
|
|
14
15
|
"browser": "./dist/lib/browser/index.mjs",
|
|
15
16
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
16
17
|
},
|
|
17
18
|
"./muxer": {
|
|
19
|
+
"source": "./src/edge-ws-muxer.ts",
|
|
18
20
|
"types": "./dist/types/src/edge-ws-muxer.d.ts",
|
|
19
21
|
"browser": "./dist/lib/browser/edge-ws-muxer.mjs",
|
|
20
22
|
"node": "./dist/lib/node-esm/edge-ws-muxer.mjs"
|
|
21
23
|
},
|
|
22
24
|
"./testing": {
|
|
25
|
+
"source": "./src/testing/index.ts",
|
|
23
26
|
"types": "./dist/types/src/testing/index.d.ts",
|
|
24
27
|
"browser": "./dist/lib/browser/testing/index.mjs",
|
|
25
28
|
"node": "./dist/lib/node-esm/testing/index.mjs"
|
|
@@ -39,24 +42,24 @@
|
|
|
39
42
|
"README.md"
|
|
40
43
|
],
|
|
41
44
|
"dependencies": {
|
|
42
|
-
"@effect/platform": "^0.
|
|
45
|
+
"@effect/platform": "^0.92.1",
|
|
43
46
|
"isomorphic-ws": "^5.0.0",
|
|
44
47
|
"ws": "^8.14.2",
|
|
45
|
-
"@dxos/async": "0.8.4-main.
|
|
46
|
-
"@dxos/context": "0.8.4-main.
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/debug": "0.8.4-main.
|
|
50
|
-
"@dxos/invariant": "0.8.4-main.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/protocols": "0.8.4-main.
|
|
56
|
-
"@dxos/util": "0.8.4-main.
|
|
48
|
+
"@dxos/async": "0.8.4-main.fffef41",
|
|
49
|
+
"@dxos/context": "0.8.4-main.fffef41",
|
|
50
|
+
"@dxos/credentials": "0.8.4-main.fffef41",
|
|
51
|
+
"@dxos/crypto": "0.8.4-main.fffef41",
|
|
52
|
+
"@dxos/debug": "0.8.4-main.fffef41",
|
|
53
|
+
"@dxos/invariant": "0.8.4-main.fffef41",
|
|
54
|
+
"@dxos/node-std": "0.8.4-main.fffef41",
|
|
55
|
+
"@dxos/log": "0.8.4-main.fffef41",
|
|
56
|
+
"@dxos/keys": "0.8.4-main.fffef41",
|
|
57
|
+
"@dxos/keyring": "0.8.4-main.fffef41",
|
|
58
|
+
"@dxos/protocols": "0.8.4-main.fffef41",
|
|
59
|
+
"@dxos/util": "0.8.4-main.fffef41"
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
|
-
"@dxos/test-utils": "0.8.4-main.
|
|
62
|
+
"@dxos/test-utils": "0.8.4-main.fffef41"
|
|
60
63
|
},
|
|
61
64
|
"peerDependencies": {
|
|
62
65
|
"effect": "^3.13.3"
|
package/src/edge-client.test.ts
CHANGED
|
@@ -41,17 +41,17 @@ describe('EdgeClient', () => {
|
|
|
41
41
|
|
|
42
42
|
const { client } = await openNewClient(endpoint);
|
|
43
43
|
|
|
44
|
-
expect(client.status).toBe(EdgeStatus.NOT_CONNECTED);
|
|
44
|
+
expect(client.status.state).toBe(EdgeStatus.ConnectionState.NOT_CONNECTED);
|
|
45
45
|
admitConnection.wake();
|
|
46
|
-
await expect.poll(() => client.status).toBe(EdgeStatus.CONNECTED);
|
|
46
|
+
await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.CONNECTED);
|
|
47
47
|
|
|
48
48
|
admitConnection.reset();
|
|
49
49
|
await closeConnection();
|
|
50
50
|
expect(client.isOpen).is.true;
|
|
51
|
-
await expect.poll(() => client.status).toBe(EdgeStatus.NOT_CONNECTED);
|
|
51
|
+
await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.NOT_CONNECTED);
|
|
52
52
|
|
|
53
53
|
admitConnection.wake();
|
|
54
|
-
await expect.poll(() => client.status).toBe(EdgeStatus.CONNECTED);
|
|
54
|
+
await expect.poll(() => client.status.state).toBe(EdgeStatus.ConnectionState.CONNECTED);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test('set identity reconnects', async () => {
|
package/src/edge-client.ts
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
Event,
|
|
7
|
+
PersistentLifecycle,
|
|
8
|
+
Trigger,
|
|
9
|
+
TriggerState,
|
|
10
|
+
scheduleMicroTask,
|
|
11
|
+
scheduleTaskInterval,
|
|
12
|
+
} from '@dxos/async';
|
|
13
|
+
import { type Lifecycle, Resource } from '@dxos/context';
|
|
7
14
|
import { log, logInfo } from '@dxos/log';
|
|
8
15
|
import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
9
16
|
import { EdgeStatus } from '@dxos/protocols/proto/dxos/client/services';
|
|
@@ -17,9 +24,19 @@ import { getEdgeUrlWithProtocol } from './utils';
|
|
|
17
24
|
|
|
18
25
|
const DEFAULT_TIMEOUT = 10_000;
|
|
19
26
|
|
|
27
|
+
// Refresh status every second: rtt, rate counters.
|
|
28
|
+
const STATUS_REFRESH_INTERVAL = 1000;
|
|
29
|
+
|
|
20
30
|
export type MessageListener = (message: Message) => void;
|
|
21
31
|
export type ReconnectListener = () => void;
|
|
22
32
|
|
|
33
|
+
export type MessengerConfig = {
|
|
34
|
+
socketEndpoint: string;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
protocol?: Protocol;
|
|
37
|
+
disableAuth?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
23
40
|
export interface EdgeConnection extends Required<Lifecycle> {
|
|
24
41
|
statusChanged: Event<EdgeStatus>;
|
|
25
42
|
get info(): any;
|
|
@@ -28,18 +45,11 @@ export interface EdgeConnection extends Required<Lifecycle> {
|
|
|
28
45
|
get isOpen(): boolean;
|
|
29
46
|
get status(): EdgeStatus;
|
|
30
47
|
setIdentity(identity: EdgeIdentity): void;
|
|
48
|
+
send(message: Message): Promise<void>;
|
|
31
49
|
onMessage(listener: MessageListener): () => void;
|
|
32
50
|
onReconnected(listener: ReconnectListener): () => void;
|
|
33
|
-
send(message: Message): Promise<void>;
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
export type MessengerConfig = {
|
|
37
|
-
socketEndpoint: string;
|
|
38
|
-
timeout?: number;
|
|
39
|
-
protocol?: Protocol;
|
|
40
|
-
disableAuth?: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
53
|
/**
|
|
44
54
|
* Messenger client for EDGE:
|
|
45
55
|
* - While open, uses PersistentLifecycle to keep an open EdgeWsConnection, reconnecting on failures.
|
|
@@ -81,9 +91,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
get status(): EdgeStatus {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
return {
|
|
95
|
+
state:
|
|
96
|
+
Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
|
|
97
|
+
? EdgeStatus.ConnectionState.CONNECTED
|
|
98
|
+
: EdgeStatus.ConnectionState.NOT_CONNECTED,
|
|
99
|
+
uptime: this._currentConnection?.uptime ?? 0,
|
|
100
|
+
rtt: this._currentConnection?.rtt ?? 0,
|
|
101
|
+
rateBytesUp: this._currentConnection?.uploadRate ?? 0,
|
|
102
|
+
rateBytesDown: this._currentConnection?.downloadRate ?? 0,
|
|
103
|
+
messagesSent: this._currentConnection?.messagesSent ?? 0,
|
|
104
|
+
messagesReceived: this._currentConnection?.messagesReceived ?? 0,
|
|
105
|
+
};
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
get identityKey() {
|
|
@@ -94,7 +113,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
94
113
|
return this._identity.peerKey;
|
|
95
114
|
}
|
|
96
115
|
|
|
97
|
-
setIdentity(identity: EdgeIdentity)
|
|
116
|
+
setIdentity(identity: EdgeIdentity) {
|
|
98
117
|
if (identity.identityKey !== this._identity.identityKey || identity.peerKey !== this._identity.peerKey) {
|
|
99
118
|
log('Edge identity changed', { identity, oldIdentity: this._identity });
|
|
100
119
|
this._identity = identity;
|
|
@@ -103,12 +122,36 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Send message.
|
|
127
|
+
* NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
|
|
128
|
+
*/
|
|
129
|
+
public async send(message: Message) {
|
|
130
|
+
if (this._ready.state !== TriggerState.RESOLVED) {
|
|
131
|
+
log('waiting for websocket');
|
|
132
|
+
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!this._currentConnection) {
|
|
136
|
+
throw new EdgeConnectionClosedError();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
message.source &&
|
|
141
|
+
(message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
|
|
142
|
+
) {
|
|
143
|
+
throw new EdgeIdentityChangedError();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this._currentConnection.send(message);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public onMessage(listener: MessageListener) {
|
|
107
150
|
this._messageListeners.add(listener);
|
|
108
151
|
return () => this._messageListeners.delete(listener);
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
public onReconnected(listener: () => void)
|
|
154
|
+
public onReconnected(listener: () => void) {
|
|
112
155
|
this._reconnectListeners.add(listener);
|
|
113
156
|
if (this._ready.state === TriggerState.RESOLVED) {
|
|
114
157
|
// Microtask so that listener is always called asynchronously, no matter the state of the ready trigger
|
|
@@ -123,6 +166,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
123
166
|
}
|
|
124
167
|
});
|
|
125
168
|
}
|
|
169
|
+
|
|
126
170
|
return () => this._reconnectListeners.delete(listener);
|
|
127
171
|
}
|
|
128
172
|
|
|
@@ -134,6 +178,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
134
178
|
this._persistentLifecycle.open().catch((err) => {
|
|
135
179
|
log.warn('Error while opening connection', { err });
|
|
136
180
|
});
|
|
181
|
+
|
|
182
|
+
// Notify about status changes (rtt, rate counters).
|
|
183
|
+
scheduleTaskInterval(
|
|
184
|
+
this._ctx,
|
|
185
|
+
async () => {
|
|
186
|
+
if (!this._currentConnection) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.statusChanged.emit(this.status);
|
|
190
|
+
},
|
|
191
|
+
STATUS_REFRESH_INTERVAL,
|
|
192
|
+
);
|
|
137
193
|
}
|
|
138
194
|
|
|
139
195
|
/**
|
|
@@ -200,7 +256,6 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
200
256
|
// Race with restartRequired so that restart is not blocked by _connect execution.
|
|
201
257
|
// Wait on ready to attempt a reconnect if it times out.
|
|
202
258
|
await Promise.race([this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }), restartRequired]);
|
|
203
|
-
|
|
204
259
|
return connection;
|
|
205
260
|
}
|
|
206
261
|
|
|
@@ -237,30 +292,6 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
237
292
|
}
|
|
238
293
|
}
|
|
239
294
|
|
|
240
|
-
/**
|
|
241
|
-
* Send message.
|
|
242
|
-
* NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
|
|
243
|
-
*/
|
|
244
|
-
public async send(message: Message): Promise<void> {
|
|
245
|
-
if (this._ready.state !== TriggerState.RESOLVED) {
|
|
246
|
-
log('waiting for websocket to become ready');
|
|
247
|
-
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!this._currentConnection) {
|
|
251
|
-
throw new EdgeConnectionClosedError();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
message.source &&
|
|
256
|
-
(message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
|
|
257
|
-
) {
|
|
258
|
-
throw new EdgeIdentityChangedError();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
this._currentConnection.send(message);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
295
|
private async _createAuthHeader(path: string): Promise<string | undefined> {
|
|
265
296
|
const httpUrl = new URL(path, this._baseHttpUrl);
|
|
266
297
|
httpUrl.protocol = getEdgeUrlWithProtocol(this._baseWsUrl.toString(), 'http');
|
|
@@ -277,7 +308,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
const encodePresentationWsAuthHeader = (encodedPresentation: Uint8Array): string => {
|
|
280
|
-
// = and / characters are not allowed in the WebSocket subprotocol header.
|
|
311
|
+
// '=' and '/' characters are not allowed in the WebSocket subprotocol header.
|
|
281
312
|
const encodedToken = Buffer.from(encodedPresentation).toString('base64').replace(/=*$/, '').replaceAll('/', '|');
|
|
282
313
|
return `base64url.bearer.authorization.dxos.org.${encodedToken}`;
|
|
283
314
|
};
|
|
@@ -11,7 +11,7 @@ import { EdgeHttpClient } from './edge-http-client';
|
|
|
11
11
|
const DEV_SERVER = 'https://edge.dxos.workers.dev';
|
|
12
12
|
|
|
13
13
|
describe.skipIf(process.env.CI)('EdgeHttpClient', () => {
|
|
14
|
-
it.
|
|
14
|
+
it.skip('should get status', async ({ expect }) => {
|
|
15
15
|
const client = new EdgeHttpClient(DEV_SERVER);
|
|
16
16
|
const identity = await createEphemeralEdgeIdentity();
|
|
17
17
|
client.setIdentity(identity);
|
package/src/edge-http-client.ts
CHANGED
|
@@ -2,47 +2,53 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
|
|
6
|
+
import * as HttpClient from '@effect/platform/HttpClient';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Function from 'effect/Function';
|
|
7
9
|
|
|
8
10
|
import { sleep } from '@dxos/async';
|
|
9
11
|
import { Context } from '@dxos/context';
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
10
13
|
import { type PublicKey, type SpaceId } from '@dxos/keys';
|
|
11
14
|
import { log } from '@dxos/log';
|
|
12
15
|
import {
|
|
13
|
-
type CreateAgentResponseBody,
|
|
14
16
|
type CreateAgentRequestBody,
|
|
17
|
+
type CreateAgentResponseBody,
|
|
15
18
|
type CreateSpaceRequest,
|
|
16
19
|
type CreateSpaceResponseBody,
|
|
17
20
|
EdgeAuthChallengeError,
|
|
18
21
|
EdgeCallFailedError,
|
|
19
|
-
type
|
|
22
|
+
type EdgeStatus,
|
|
20
23
|
type ExecuteWorkflowResponseBody,
|
|
24
|
+
type ExportBundleRequest,
|
|
25
|
+
type ExportBundleResponse,
|
|
21
26
|
type GetAgentStatusResponseBody,
|
|
22
27
|
type GetNotarizationResponseBody,
|
|
28
|
+
type ImportBundleRequest,
|
|
23
29
|
type InitiateOAuthFlowRequest,
|
|
24
30
|
type InitiateOAuthFlowResponse,
|
|
25
31
|
type JoinSpaceRequest,
|
|
26
32
|
type JoinSpaceResponseBody,
|
|
27
|
-
type RecoverIdentityRequest,
|
|
28
|
-
type RecoverIdentityResponseBody,
|
|
29
33
|
type ObjectId,
|
|
30
34
|
type PostNotarizationRequestBody,
|
|
31
|
-
type QueueQuery,
|
|
32
35
|
type QueryResult,
|
|
36
|
+
type QueueQuery,
|
|
37
|
+
type RecoverIdentityRequest,
|
|
38
|
+
type RecoverIdentityResponseBody,
|
|
33
39
|
type UploadFunctionRequest,
|
|
34
40
|
type UploadFunctionResponseBody,
|
|
35
|
-
type EdgeStatus,
|
|
36
41
|
} from '@dxos/protocols';
|
|
37
42
|
import { createUrl } from '@dxos/util';
|
|
38
43
|
|
|
39
44
|
import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
|
|
40
|
-
import {
|
|
45
|
+
import { HttpConfig, encodeAuthHeader, withLogging, withRetryConfig } from './http-client';
|
|
41
46
|
import { getEdgeUrlWithProtocol } from './utils';
|
|
42
47
|
|
|
43
48
|
const DEFAULT_RETRY_TIMEOUT = 1500;
|
|
44
49
|
const DEFAULT_RETRY_JITTER = 500;
|
|
45
50
|
const DEFAULT_MAX_RETRIES_COUNT = 3;
|
|
51
|
+
const WARNING_BODY_SIZE = 10 * 1024 * 1024; // 10MB
|
|
46
52
|
|
|
47
53
|
export type RetryConfig = {
|
|
48
54
|
/**
|
|
@@ -64,10 +70,27 @@ type EdgeHttpRequestArgs = {
|
|
|
64
70
|
context?: Context;
|
|
65
71
|
retry?: RetryConfig;
|
|
66
72
|
body?: any;
|
|
73
|
+
/**
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
json?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Do not expect a standard EDGE JSON response with a `success` field.
|
|
80
|
+
* @deprecated Use only for debugging.
|
|
81
|
+
*/
|
|
82
|
+
rawResponse?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Force authentication.
|
|
86
|
+
* This should be used for requests with large bodies to avoid sending the body twice.
|
|
87
|
+
* The client will call /auth endpoint to generate the auth header.
|
|
88
|
+
*/
|
|
89
|
+
auth?: boolean;
|
|
67
90
|
};
|
|
68
91
|
|
|
69
|
-
export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry'>;
|
|
70
|
-
export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body'>;
|
|
92
|
+
export type EdgeHttpGetArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'auth'>;
|
|
93
|
+
export type EdgeHttpPostArgs = Pick<EdgeHttpRequestArgs, 'context' | 'retry' | 'body' | 'auth'>;
|
|
71
94
|
|
|
72
95
|
export class EdgeHttpClient {
|
|
73
96
|
private readonly _baseUrl: string;
|
|
@@ -246,8 +269,64 @@ export class EdgeHttpClient {
|
|
|
246
269
|
body: UploadFunctionRequest,
|
|
247
270
|
args?: EdgeHttpGetArgs,
|
|
248
271
|
): Promise<UploadFunctionResponseBody> {
|
|
272
|
+
const formData = new FormData();
|
|
273
|
+
formData.append('name', body.name ?? '');
|
|
274
|
+
formData.append('version', body.version);
|
|
275
|
+
formData.append('ownerPublicKey', body.ownerPublicKey);
|
|
276
|
+
formData.append('entryPoint', body.entryPoint);
|
|
277
|
+
body.runtime && formData.append('runtime', body.runtime);
|
|
278
|
+
for (const [filename, content] of Object.entries(body.assets)) {
|
|
279
|
+
formData.append(
|
|
280
|
+
'assets',
|
|
281
|
+
new Blob([content as Uint8Array<ArrayBuffer>], { type: getFileMimeType(filename) }),
|
|
282
|
+
filename,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
249
286
|
const path = ['functions', ...(pathParts.functionId ? [pathParts.functionId] : [])].join('/');
|
|
250
|
-
return this._call(new URL(path, this.baseUrl), {
|
|
287
|
+
return this._call(new URL(path, this.baseUrl), {
|
|
288
|
+
...args,
|
|
289
|
+
body: formData,
|
|
290
|
+
method: 'PUT',
|
|
291
|
+
json: false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public async listFunctions(args?: EdgeHttpGetArgs): Promise<any> {
|
|
296
|
+
return this._call(new URL('/functions', this.baseUrl), { ...args, method: 'GET' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public async invokeFunction(
|
|
300
|
+
params: {
|
|
301
|
+
functionId: string;
|
|
302
|
+
version?: string;
|
|
303
|
+
spaceId?: SpaceId;
|
|
304
|
+
cpuTimeLimit?: number;
|
|
305
|
+
subrequestsLimit?: number;
|
|
306
|
+
},
|
|
307
|
+
input: unknown,
|
|
308
|
+
args?: EdgeHttpGetArgs,
|
|
309
|
+
): Promise<any> {
|
|
310
|
+
const url = new URL(`/functions/${params.functionId}`, this.baseUrl);
|
|
311
|
+
if (params.version) {
|
|
312
|
+
url.searchParams.set('version', params.version);
|
|
313
|
+
}
|
|
314
|
+
if (params.spaceId) {
|
|
315
|
+
url.searchParams.set('spaceId', params.spaceId.toString());
|
|
316
|
+
}
|
|
317
|
+
if (params.cpuTimeLimit) {
|
|
318
|
+
url.searchParams.set('cpuTimeLimit', params.cpuTimeLimit.toString());
|
|
319
|
+
}
|
|
320
|
+
if (params.subrequestsLimit) {
|
|
321
|
+
url.searchParams.set('subrequestsLimit', params.subrequestsLimit.toString());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return this._call(url, {
|
|
325
|
+
...args,
|
|
326
|
+
body: input,
|
|
327
|
+
method: 'POST',
|
|
328
|
+
rawResponse: true,
|
|
329
|
+
});
|
|
251
330
|
}
|
|
252
331
|
|
|
253
332
|
//
|
|
@@ -267,12 +346,44 @@ export class EdgeHttpClient {
|
|
|
267
346
|
});
|
|
268
347
|
}
|
|
269
348
|
|
|
349
|
+
//
|
|
350
|
+
// Triggers
|
|
351
|
+
//
|
|
352
|
+
|
|
353
|
+
public async getCronTriggers(spaceId: SpaceId) {
|
|
354
|
+
return this._call(new URL(`/test/functions/${spaceId}/triggers/crons`, this.baseUrl), { method: 'GET' });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//
|
|
358
|
+
// Import/Export space.
|
|
359
|
+
//
|
|
360
|
+
|
|
361
|
+
public async importBundle(
|
|
362
|
+
spaceId: SpaceId, //
|
|
363
|
+
body: ImportBundleRequest,
|
|
364
|
+
args?: EdgeHttpGetArgs,
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
return this._call(new URL(`/spaces/${spaceId}/import`, this.baseUrl), { ...args, body, method: 'PUT' });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public async exportBundle(
|
|
370
|
+
spaceId: SpaceId,
|
|
371
|
+
body: ExportBundleRequest,
|
|
372
|
+
args?: EdgeHttpGetArgs,
|
|
373
|
+
): Promise<ExportBundleResponse> {
|
|
374
|
+
return this._call(new URL(`/spaces/${spaceId}/export`, this.baseUrl), {
|
|
375
|
+
...args,
|
|
376
|
+
body,
|
|
377
|
+
method: 'POST',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
270
381
|
//
|
|
271
382
|
// Internal
|
|
272
383
|
//
|
|
273
384
|
|
|
274
|
-
private async _fetch<T>(url: URL,
|
|
275
|
-
return pipe(
|
|
385
|
+
private async _fetch<T>(url: URL, _args: EdgeHttpRequestArgs): Promise<T> {
|
|
386
|
+
return Function.pipe(
|
|
276
387
|
HttpClient.get(url),
|
|
277
388
|
withLogging,
|
|
278
389
|
withRetryConfig,
|
|
@@ -286,44 +397,64 @@ export class EdgeHttpClient {
|
|
|
286
397
|
// TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
|
|
287
398
|
private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
|
|
288
399
|
const shouldRetry = createRetryHandler(args);
|
|
289
|
-
const requestContext = args.context ??
|
|
400
|
+
const requestContext = args.context ?? Context.default();
|
|
290
401
|
log('fetch', { url, request: args.body });
|
|
291
402
|
|
|
292
403
|
let handledAuth = false;
|
|
404
|
+
const tryCount = 1;
|
|
293
405
|
while (true) {
|
|
294
|
-
let processingError: EdgeCallFailedError;
|
|
295
|
-
let retryAfterHeaderValue: number = Number.NaN;
|
|
406
|
+
let processingError: EdgeCallFailedError | undefined = undefined;
|
|
296
407
|
try {
|
|
408
|
+
if (!this._authHeader && args.auth) {
|
|
409
|
+
const response = await fetch(new URL(`/auth`, this.baseUrl));
|
|
410
|
+
if (response.status === 401) {
|
|
411
|
+
this._authHeader = await this._handleUnauthorized(response);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
297
415
|
const request = createRequest(args, this._authHeader);
|
|
416
|
+
log('call edge', { url, tryCount, authHeader: !!this._authHeader });
|
|
298
417
|
const response = await fetch(url, request);
|
|
299
|
-
|
|
418
|
+
|
|
300
419
|
if (response.ok) {
|
|
301
|
-
const body =
|
|
420
|
+
const body = await response.clone().json();
|
|
421
|
+
if (args.rawResponse) {
|
|
422
|
+
return body as any;
|
|
423
|
+
}
|
|
424
|
+
invariant(body, 'Expected body to be present');
|
|
425
|
+
if (!('success' in body)) {
|
|
426
|
+
return body;
|
|
427
|
+
}
|
|
302
428
|
if (body.success) {
|
|
303
429
|
return body.data;
|
|
304
430
|
}
|
|
305
|
-
|
|
306
|
-
log.warn('unsuccessful edge response', { url, body });
|
|
307
|
-
if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
|
|
308
|
-
processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
|
|
309
|
-
} else {
|
|
310
|
-
processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
|
|
311
|
-
}
|
|
312
431
|
} else if (response.status === 401 && !handledAuth) {
|
|
313
432
|
this._authHeader = await this._handleUnauthorized(response);
|
|
314
433
|
handledAuth = true;
|
|
315
434
|
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const body =
|
|
438
|
+
response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
|
|
439
|
+
|
|
440
|
+
invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
|
|
441
|
+
|
|
442
|
+
if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
|
|
443
|
+
processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
|
|
444
|
+
} else if (body?.success === false) {
|
|
445
|
+
processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
|
|
316
446
|
} else {
|
|
317
|
-
|
|
447
|
+
invariant(!response.ok, 'Expected response to not be ok.');
|
|
448
|
+
processingError = await EdgeCallFailedError.fromHttpFailure(response);
|
|
318
449
|
}
|
|
319
450
|
} catch (error: any) {
|
|
320
451
|
processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
|
|
321
452
|
}
|
|
322
453
|
|
|
323
|
-
if (processingError
|
|
324
|
-
log('retrying edge request', { url, processingError });
|
|
454
|
+
if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
|
|
455
|
+
log.verbose('retrying edge request', { url, processingError });
|
|
325
456
|
} else {
|
|
326
|
-
throw processingError
|
|
457
|
+
throw processingError!;
|
|
327
458
|
}
|
|
328
459
|
}
|
|
329
460
|
}
|
|
@@ -331,7 +462,7 @@ export class EdgeHttpClient {
|
|
|
331
462
|
private async _handleUnauthorized(response: Response): Promise<string> {
|
|
332
463
|
if (!this._edgeIdentity) {
|
|
333
464
|
log.warn('unauthorized response received before identity was set');
|
|
334
|
-
throw EdgeCallFailedError.fromHttpFailure(response);
|
|
465
|
+
throw await EdgeCallFailedError.fromHttpFailure(response);
|
|
335
466
|
}
|
|
336
467
|
|
|
337
468
|
const challenge = await handleAuthChallenge(response, this._edgeIdentity);
|
|
@@ -339,11 +470,32 @@ export class EdgeHttpClient {
|
|
|
339
470
|
}
|
|
340
471
|
}
|
|
341
472
|
|
|
342
|
-
const createRequest = (
|
|
473
|
+
const createRequest = (
|
|
474
|
+
{ method, body, json = true }: EdgeHttpRequestArgs,
|
|
475
|
+
authHeader: string | undefined,
|
|
476
|
+
): RequestInit => {
|
|
477
|
+
let requestBody: BodyInit | undefined;
|
|
478
|
+
const headers: HeadersInit = {};
|
|
479
|
+
|
|
480
|
+
if (json) {
|
|
481
|
+
requestBody = body && JSON.stringify(body);
|
|
482
|
+
headers['Content-Type'] = 'application/json';
|
|
483
|
+
} else {
|
|
484
|
+
requestBody = body;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (typeof requestBody === 'string' && requestBody.length > WARNING_BODY_SIZE) {
|
|
488
|
+
log.warn('Request with large body', { bodySize: requestBody.length });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (authHeader) {
|
|
492
|
+
headers['Authorization'] = authHeader;
|
|
493
|
+
}
|
|
494
|
+
|
|
343
495
|
return {
|
|
344
496
|
method,
|
|
345
|
-
body:
|
|
346
|
-
headers
|
|
497
|
+
body: requestBody,
|
|
498
|
+
headers,
|
|
347
499
|
};
|
|
348
500
|
};
|
|
349
501
|
|
|
@@ -359,7 +511,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
|
|
|
359
511
|
const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
|
|
360
512
|
const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
|
|
361
513
|
const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
|
|
362
|
-
return async (ctx: Context, retryAfter
|
|
514
|
+
return async (ctx: Context, retryAfter?: number) => {
|
|
363
515
|
if (++retries > maxRetries || ctx.disposed) {
|
|
364
516
|
return false;
|
|
365
517
|
}
|
|
@@ -374,3 +526,10 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
|
|
|
374
526
|
return true;
|
|
375
527
|
};
|
|
376
528
|
};
|
|
529
|
+
|
|
530
|
+
const getFileMimeType = (filename: string) =>
|
|
531
|
+
['.js', '.mjs'].some((codeExtension) => filename.endsWith(codeExtension))
|
|
532
|
+
? 'application/javascript+module'
|
|
533
|
+
: filename.endsWith('.wasm')
|
|
534
|
+
? 'application/wasm'
|
|
535
|
+
: 'application/octet-stream';
|