@dxos/edge-client 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
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/LICENSE +102 -5
- package/dist/lib/{node-esm/chunk-R6K4IIBW.mjs → neutral/chunk-L5ZHLJ4B.mjs} +57 -53
- package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs.map +7 -0
- package/dist/lib/neutral/chunk-WQKMEZJR.mjs +30 -0
- package/dist/lib/neutral/chunk-WQKMEZJR.mjs.map +7 -0
- package/dist/lib/neutral/cors-proxy.mjs +7 -0
- package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
- package/dist/lib/neutral/index.mjs +1448 -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 +6 -31
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/auth.d.ts.map +1 -1
- package/dist/types/src/base-http-client.d.ts +48 -0
- package/dist/types/src/base-http-client.d.ts.map +1 -0
- package/dist/types/src/cors-proxy.d.ts +6 -0
- package/dist/types/src/cors-proxy.d.ts.map +1 -0
- package/dist/types/src/edge-ai-http-client.d.ts +65 -0
- package/dist/types/src/edge-ai-http-client.d.ts.map +1 -0
- package/dist/types/src/edge-client.d.ts +17 -14
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +81 -54
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- package/dist/types/src/edge-identity.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/errors.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/hub-http-client.d.ts +39 -0
- package/dist/types/src/hub-http-client.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +8 -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/test-server.d.ts.map +1 -1
- 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 -32
- package/src/base-http-client.ts +243 -0
- package/src/cors-proxy.ts +38 -0
- package/src/edge-ai-http-client.ts +129 -0
- package/src/edge-client.test.ts +20 -15
- package/src/edge-client.ts +105 -44
- package/src/edge-http-client.test.ts +37 -3
- package/src/edge-http-client.ts +312 -190
- package/src/edge-ws-connection.ts +120 -6
- package/src/edge-ws-muxer.ts +49 -5
- package/src/http-client.test.ts +11 -7
- package/src/http-client.ts +18 -8
- package/src/hub-http-client.ts +118 -0
- package/src/index.ts +8 -3
- package/src/testing/test-utils.ts +7 -7
- package/dist/lib/browser/chunk-SUXH7FH6.mjs +0 -304
- package/dist/lib/browser/chunk-SUXH7FH6.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -1124
- 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-esm/chunk-R6K4IIBW.mjs.map +0 -7
- package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
- package/dist/lib/node-esm/index.mjs +0 -1125
- 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 -186
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- /package/dist/lib/{browser/edge-ws-muxer.mjs.map → neutral/cors-proxy.mjs.map} +0 -0
- /package/dist/lib/{node-esm → neutral}/edge-ws-muxer.mjs.map +0 -0
package/src/edge-client.ts
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Event,
|
|
7
|
+
PersistentLifecycle,
|
|
8
|
+
Trigger,
|
|
9
|
+
TriggerState,
|
|
10
|
+
scheduleMicroTask,
|
|
11
|
+
scheduleTaskInterval,
|
|
12
|
+
} from '@dxos/async';
|
|
13
|
+
import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
|
|
6
14
|
import { type Lifecycle, Resource } from '@dxos/context';
|
|
7
15
|
import { log, logInfo } from '@dxos/log';
|
|
8
16
|
import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
@@ -17,9 +25,21 @@ import { getEdgeUrlWithProtocol } from './utils';
|
|
|
17
25
|
|
|
18
26
|
const DEFAULT_TIMEOUT = 10_000;
|
|
19
27
|
|
|
28
|
+
// Refresh status every second: rtt, rate counters.
|
|
29
|
+
const STATUS_REFRESH_INTERVAL = 1000;
|
|
30
|
+
|
|
20
31
|
export type MessageListener = (message: Message) => void;
|
|
21
32
|
export type ReconnectListener = () => void;
|
|
22
33
|
|
|
34
|
+
export type MessengerConfig = {
|
|
35
|
+
socketEndpoint: string;
|
|
36
|
+
timeout?: number;
|
|
37
|
+
protocol?: Protocol;
|
|
38
|
+
disableAuth?: boolean;
|
|
39
|
+
/** Sent as `X-DXOS-Client-Tag` on the WebSocket upgrade (Node/`ws` only; ignored in browsers). */
|
|
40
|
+
clientTag?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
23
43
|
export interface EdgeConnection extends Required<Lifecycle> {
|
|
24
44
|
statusChanged: Event<EdgeStatus>;
|
|
25
45
|
get info(): any;
|
|
@@ -28,18 +48,11 @@ export interface EdgeConnection extends Required<Lifecycle> {
|
|
|
28
48
|
get isOpen(): boolean;
|
|
29
49
|
get status(): EdgeStatus;
|
|
30
50
|
setIdentity(identity: EdgeIdentity): void;
|
|
51
|
+
send(ctx: Context, message: Message): Promise<void>;
|
|
31
52
|
onMessage(listener: MessageListener): () => void;
|
|
32
53
|
onReconnected(listener: ReconnectListener): () => void;
|
|
33
|
-
send(message: Message): Promise<void>;
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
export type MessengerConfig = {
|
|
37
|
-
socketEndpoint: string;
|
|
38
|
-
timeout?: number;
|
|
39
|
-
protocol?: Protocol;
|
|
40
|
-
disableAuth?: boolean;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
56
|
/**
|
|
44
57
|
* Messenger client for EDGE:
|
|
45
58
|
* - While open, uses PersistentLifecycle to keep an open EdgeWsConnection, reconnecting on failures.
|
|
@@ -81,9 +94,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
get status(): EdgeStatus {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
return {
|
|
98
|
+
state:
|
|
99
|
+
Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
|
|
100
|
+
? EdgeStatus.ConnectionState.CONNECTED
|
|
101
|
+
: EdgeStatus.ConnectionState.NOT_CONNECTED,
|
|
102
|
+
uptime: this._currentConnection?.uptime ?? 0,
|
|
103
|
+
rtt: this._currentConnection?.rtt ?? 0,
|
|
104
|
+
rateBytesUp: this._currentConnection?.uploadRate ?? 0,
|
|
105
|
+
rateBytesDown: this._currentConnection?.downloadRate ?? 0,
|
|
106
|
+
messagesSent: this._currentConnection?.messagesSent ?? 0,
|
|
107
|
+
messagesReceived: this._currentConnection?.messagesReceived ?? 0,
|
|
108
|
+
};
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
get identityKey() {
|
|
@@ -94,7 +116,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
94
116
|
return this._identity.peerKey;
|
|
95
117
|
}
|
|
96
118
|
|
|
97
|
-
setIdentity(identity: EdgeIdentity)
|
|
119
|
+
setIdentity(identity: EdgeIdentity) {
|
|
98
120
|
if (identity.identityKey !== this._identity.identityKey || identity.peerKey !== this._identity.peerKey) {
|
|
99
121
|
log('Edge identity changed', { identity, oldIdentity: this._identity });
|
|
100
122
|
this._identity = identity;
|
|
@@ -103,12 +125,45 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
103
125
|
}
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Send message.
|
|
130
|
+
* NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
|
|
131
|
+
*/
|
|
132
|
+
public async send(ctx: Context, message: Message) {
|
|
133
|
+
if (this._ready.state !== TriggerState.RESOLVED) {
|
|
134
|
+
log('waiting for websocket');
|
|
135
|
+
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!this._currentConnection) {
|
|
139
|
+
throw new EdgeConnectionClosedError();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
message.source &&
|
|
144
|
+
(message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
|
|
145
|
+
) {
|
|
146
|
+
throw new EdgeIdentityChangedError();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const traceCtx = ctx.getAttribute(TRACE_SPAN_ATTRIBUTE) as TraceContextData | undefined;
|
|
150
|
+
if (traceCtx) {
|
|
151
|
+
message.traceContext = {
|
|
152
|
+
$typeName: 'dxos.edge.messenger.TraceContext',
|
|
153
|
+
traceparent: traceCtx.traceparent,
|
|
154
|
+
tracestate: traceCtx.tracestate,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._currentConnection.send(message);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public onMessage(listener: MessageListener) {
|
|
107
162
|
this._messageListeners.add(listener);
|
|
108
163
|
return () => this._messageListeners.delete(listener);
|
|
109
164
|
}
|
|
110
165
|
|
|
111
|
-
public onReconnected(listener:
|
|
166
|
+
public onReconnected(listener: ReconnectListener) {
|
|
112
167
|
this._reconnectListeners.add(listener);
|
|
113
168
|
if (this._ready.state === TriggerState.RESOLVED) {
|
|
114
169
|
// Microtask so that listener is always called asynchronously, no matter the state of the ready trigger
|
|
@@ -123,6 +178,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
123
178
|
}
|
|
124
179
|
});
|
|
125
180
|
}
|
|
181
|
+
|
|
126
182
|
return () => this._reconnectListeners.delete(listener);
|
|
127
183
|
}
|
|
128
184
|
|
|
@@ -134,6 +190,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
134
190
|
this._persistentLifecycle.open().catch((err) => {
|
|
135
191
|
log.warn('Error while opening connection', { err });
|
|
136
192
|
});
|
|
193
|
+
|
|
194
|
+
// Notify about status changes (rtt, rate counters).
|
|
195
|
+
scheduleTaskInterval(
|
|
196
|
+
this._ctx,
|
|
197
|
+
async () => {
|
|
198
|
+
if (!this._currentConnection) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.statusChanged.emit(this.status);
|
|
202
|
+
},
|
|
203
|
+
STATUS_REFRESH_INTERVAL,
|
|
204
|
+
);
|
|
137
205
|
}
|
|
138
206
|
|
|
139
207
|
/**
|
|
@@ -163,7 +231,11 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
163
231
|
log('Opening websocket', { url: url.toString(), protocolHeader });
|
|
164
232
|
const connection = new EdgeWsConnection(
|
|
165
233
|
identity,
|
|
166
|
-
{
|
|
234
|
+
{
|
|
235
|
+
url,
|
|
236
|
+
protocolHeader,
|
|
237
|
+
headers: this._config.clientTag ? { 'X-DXOS-Client-Tag': this._config.clientTag } : undefined,
|
|
238
|
+
},
|
|
167
239
|
{
|
|
168
240
|
onConnected: () => {
|
|
169
241
|
if (this._isActive(connection)) {
|
|
@@ -197,9 +269,22 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
197
269
|
this._currentConnection = connection;
|
|
198
270
|
|
|
199
271
|
await connection.open();
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
272
|
+
|
|
273
|
+
// The connection is only a successful start once the socket becomes ready. A socket that
|
|
274
|
+
// closes or errors before becoming ready (or never connects within the timeout) is a failed
|
|
275
|
+
// start: throwing lets PersistentLifecycle apply its backoff. Returning here would mark the
|
|
276
|
+
// attempt as successful and reset the backoff, degenerating reconnects into a hot loop when
|
|
277
|
+
// the server accepts then immediately drops the socket.
|
|
278
|
+
const becameReady = await Promise.race([
|
|
279
|
+
this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }).then(
|
|
280
|
+
() => true,
|
|
281
|
+
() => false,
|
|
282
|
+
),
|
|
283
|
+
restartRequired.wait().then(() => false),
|
|
284
|
+
]);
|
|
285
|
+
if (!becameReady) {
|
|
286
|
+
throw new EdgeConnectionClosedError();
|
|
287
|
+
}
|
|
203
288
|
|
|
204
289
|
return connection;
|
|
205
290
|
}
|
|
@@ -237,30 +322,6 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
237
322
|
}
|
|
238
323
|
}
|
|
239
324
|
|
|
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
325
|
private async _createAuthHeader(path: string): Promise<string | undefined> {
|
|
265
326
|
const httpUrl = new URL(path, this._baseHttpUrl);
|
|
266
327
|
httpUrl.protocol = getEdgeUrlWithProtocol(this._baseWsUrl.toString(), 'http');
|
|
@@ -277,7 +338,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
277
338
|
}
|
|
278
339
|
|
|
279
340
|
const encodePresentationWsAuthHeader = (encodedPresentation: Uint8Array): string => {
|
|
280
|
-
// = and / characters are not allowed in the WebSocket subprotocol header.
|
|
341
|
+
// '=' and '/' characters are not allowed in the WebSocket subprotocol header.
|
|
281
342
|
const encodedToken = Buffer.from(encodedPresentation).toString('base64').replace(/=*$/, '').replaceAll('/', '|');
|
|
282
343
|
return `base64url.bearer.authorization.dxos.org.${encodedToken}`;
|
|
283
344
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe, it } from 'vitest';
|
|
5
|
+
import { afterEach, describe, it, test, vi } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { createEphemeralEdgeIdentity } from './auth';
|
|
8
8
|
import { EdgeHttpClient } from './edge-http-client';
|
|
@@ -11,12 +11,46 @@ 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);
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const { Context } = await import('@dxos/context');
|
|
20
|
+
const result = await client.getStatus(Context.default());
|
|
20
21
|
expect(result).toBeDefined();
|
|
21
22
|
});
|
|
22
23
|
});
|
|
24
|
+
|
|
25
|
+
describe('EdgeHttpClient.anthropicAiRequest', () => {
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.unstubAllGlobals();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('re-bases the request path onto the EDGE /ai/generate/anthropic route', async ({ expect }) => {
|
|
31
|
+
const fetchMock = vi.fn(async (input: any, _init?: RequestInit) => {
|
|
32
|
+
const url = String(input instanceof URL ? input : (input.url ?? input));
|
|
33
|
+
// `/auth` preflight: respond non-401 so no auth header is attached.
|
|
34
|
+
if (url.endsWith('/auth')) {
|
|
35
|
+
return new Response(null, { status: 200 });
|
|
36
|
+
}
|
|
37
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
38
|
+
});
|
|
39
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
40
|
+
|
|
41
|
+
const client = new EdgeHttpClient('https://edge.example.com');
|
|
42
|
+
const response = await client.anthropicAiRequest(
|
|
43
|
+
new Request('http://edge/v1/messages?beta=true', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: JSON.stringify({ model: 'claude' }),
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(response.status).toBe(200);
|
|
50
|
+
|
|
51
|
+
const targetCall = fetchMock.mock.calls.find((call) => !String(call[0]).endsWith('/auth'));
|
|
52
|
+
expect(targetCall).toBeDefined();
|
|
53
|
+
expect(String(targetCall![0])).toBe('https://edge.example.com/ai/generate/anthropic/v1/messages?beta=true');
|
|
54
|
+
expect(targetCall![1]?.method).toBe('POST');
|
|
55
|
+
});
|
|
56
|
+
});
|