@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.
Files changed (72) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{node-esm/chunk-R6K4IIBW.mjs → neutral/chunk-L5ZHLJ4B.mjs} +57 -53
  3. package/dist/lib/neutral/chunk-L5ZHLJ4B.mjs.map +7 -0
  4. package/dist/lib/neutral/chunk-WQKMEZJR.mjs +30 -0
  5. package/dist/lib/neutral/chunk-WQKMEZJR.mjs.map +7 -0
  6. package/dist/lib/neutral/cors-proxy.mjs +7 -0
  7. package/dist/lib/{browser → neutral}/edge-ws-muxer.mjs +1 -1
  8. package/dist/lib/neutral/index.mjs +1448 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/{browser → neutral}/testing/index.mjs +6 -31
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/auth.d.ts.map +1 -1
  14. package/dist/types/src/base-http-client.d.ts +48 -0
  15. package/dist/types/src/base-http-client.d.ts.map +1 -0
  16. package/dist/types/src/cors-proxy.d.ts +6 -0
  17. package/dist/types/src/cors-proxy.d.ts.map +1 -0
  18. package/dist/types/src/edge-ai-http-client.d.ts +65 -0
  19. package/dist/types/src/edge-ai-http-client.d.ts.map +1 -0
  20. package/dist/types/src/edge-client.d.ts +17 -14
  21. package/dist/types/src/edge-client.d.ts.map +1 -1
  22. package/dist/types/src/edge-http-client.d.ts +81 -54
  23. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  24. package/dist/types/src/edge-identity.d.ts.map +1 -1
  25. package/dist/types/src/edge-ws-connection.d.ts +20 -0
  26. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  27. package/dist/types/src/edge-ws-muxer.d.ts.map +1 -1
  28. package/dist/types/src/errors.d.ts.map +1 -1
  29. package/dist/types/src/http-client.d.ts +10 -7
  30. package/dist/types/src/http-client.d.ts.map +1 -1
  31. package/dist/types/src/hub-http-client.d.ts +39 -0
  32. package/dist/types/src/hub-http-client.d.ts.map +1 -0
  33. package/dist/types/src/index.d.ts +8 -3
  34. package/dist/types/src/index.d.ts.map +1 -1
  35. package/dist/types/src/protocol.d.ts +1 -1
  36. package/dist/types/src/protocol.d.ts.map +1 -1
  37. package/dist/types/src/testing/test-server.d.ts.map +1 -1
  38. package/dist/types/src/testing/test-utils.d.ts +3 -3
  39. package/dist/types/src/testing/test-utils.d.ts.map +1 -1
  40. package/dist/types/src/utils.d.ts +1 -1
  41. package/dist/types/src/utils.d.ts.map +1 -1
  42. package/dist/types/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +33 -32
  44. package/src/base-http-client.ts +243 -0
  45. package/src/cors-proxy.ts +38 -0
  46. package/src/edge-ai-http-client.ts +129 -0
  47. package/src/edge-client.test.ts +20 -15
  48. package/src/edge-client.ts +105 -44
  49. package/src/edge-http-client.test.ts +37 -3
  50. package/src/edge-http-client.ts +312 -190
  51. package/src/edge-ws-connection.ts +120 -6
  52. package/src/edge-ws-muxer.ts +49 -5
  53. package/src/http-client.test.ts +11 -7
  54. package/src/http-client.ts +18 -8
  55. package/src/hub-http-client.ts +118 -0
  56. package/src/index.ts +8 -3
  57. package/src/testing/test-utils.ts +7 -7
  58. package/dist/lib/browser/chunk-SUXH7FH6.mjs +0 -304
  59. package/dist/lib/browser/chunk-SUXH7FH6.mjs.map +0 -7
  60. package/dist/lib/browser/index.mjs +0 -1124
  61. package/dist/lib/browser/index.mjs.map +0 -7
  62. package/dist/lib/browser/meta.json +0 -1
  63. package/dist/lib/browser/testing/index.mjs.map +0 -7
  64. package/dist/lib/node-esm/chunk-R6K4IIBW.mjs.map +0 -7
  65. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  66. package/dist/lib/node-esm/index.mjs +0 -1125
  67. package/dist/lib/node-esm/index.mjs.map +0 -7
  68. package/dist/lib/node-esm/meta.json +0 -1
  69. package/dist/lib/node-esm/testing/index.mjs +0 -186
  70. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  71. /package/dist/lib/{browser/edge-ws-muxer.mjs.map → neutral/cors-proxy.mjs.map} +0 -0
  72. /package/dist/lib/{node-esm → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -2,7 +2,15 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Event, PersistentLifecycle, Trigger, TriggerState, scheduleMicroTask } from '@dxos/async';
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 Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
85
- ? EdgeStatus.CONNECTED
86
- : EdgeStatus.NOT_CONNECTED;
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): void {
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
- public onMessage(listener: MessageListener): () => void {
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: () => void): () => void {
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
- { url, protocolHeader },
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
- // Race with restartRequired so that restart is not blocked by _connect execution.
201
- // Wait on ready to attempt a reconnect if it times out.
202
- await Promise.race([this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }), restartRequired]);
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.only('should get status', async ({ expect }) => {
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 result = await client.getStatus();
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
+ });