@dxos/edge-client 0.8.4-main.fffef41 → 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 (70) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/{node-esm/chunk-JTBFRYNM.mjs → neutral/chunk-L5ZHLJ4B.mjs} +54 -47
  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/{browser → neutral}/index.mjs +553 -467
  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 +6 -3
  21. package/dist/types/src/edge-client.d.ts.map +1 -1
  22. package/dist/types/src/edge-http-client.d.ts +76 -75
  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 +1 -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 +2 -2
  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 +4 -0
  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 +2 -2
  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 +16 -11
  48. package/src/edge-client.ts +37 -7
  49. package/src/edge-http-client.test.ts +36 -2
  50. package/src/edge-http-client.ts +237 -270
  51. package/src/edge-ws-connection.ts +2 -1
  52. package/src/edge-ws-muxer.ts +49 -5
  53. package/src/http-client.test.ts +3 -2
  54. package/src/hub-http-client.ts +118 -0
  55. package/src/index.ts +4 -0
  56. package/src/testing/test-utils.ts +4 -4
  57. package/dist/lib/browser/chunk-VESGVCLQ.mjs +0 -301
  58. package/dist/lib/browser/chunk-VESGVCLQ.mjs.map +0 -7
  59. package/dist/lib/browser/index.mjs.map +0 -7
  60. package/dist/lib/browser/meta.json +0 -1
  61. package/dist/lib/browser/testing/index.mjs.map +0 -7
  62. package/dist/lib/node-esm/chunk-JTBFRYNM.mjs.map +0 -7
  63. package/dist/lib/node-esm/edge-ws-muxer.mjs +0 -12
  64. package/dist/lib/node-esm/index.mjs +0 -1363
  65. package/dist/lib/node-esm/index.mjs.map +0 -7
  66. package/dist/lib/node-esm/meta.json +0 -1
  67. package/dist/lib/node-esm/testing/index.mjs +0 -186
  68. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  69. /package/dist/lib/{browser/edge-ws-muxer.mjs.map → neutral/cors-proxy.mjs.map} +0 -0
  70. /package/dist/lib/{node-esm → neutral}/edge-ws-muxer.mjs.map +0 -0
@@ -10,6 +10,7 @@ import {
10
10
  scheduleMicroTask,
11
11
  scheduleTaskInterval,
12
12
  } from '@dxos/async';
13
+ import { Context, TRACE_SPAN_ATTRIBUTE, type TraceContextData } from '@dxos/context';
13
14
  import { type Lifecycle, Resource } from '@dxos/context';
14
15
  import { log, logInfo } from '@dxos/log';
15
16
  import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
@@ -35,6 +36,8 @@ export type MessengerConfig = {
35
36
  timeout?: number;
36
37
  protocol?: Protocol;
37
38
  disableAuth?: boolean;
39
+ /** Sent as `X-DXOS-Client-Tag` on the WebSocket upgrade (Node/`ws` only; ignored in browsers). */
40
+ clientTag?: string;
38
41
  };
39
42
 
40
43
  export interface EdgeConnection extends Required<Lifecycle> {
@@ -45,7 +48,7 @@ export interface EdgeConnection extends Required<Lifecycle> {
45
48
  get isOpen(): boolean;
46
49
  get status(): EdgeStatus;
47
50
  setIdentity(identity: EdgeIdentity): void;
48
- send(message: Message): Promise<void>;
51
+ send(ctx: Context, message: Message): Promise<void>;
49
52
  onMessage(listener: MessageListener): () => void;
50
53
  onReconnected(listener: ReconnectListener): () => void;
51
54
  }
@@ -126,7 +129,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
126
129
  * Send message.
127
130
  * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
128
131
  */
129
- public async send(message: Message) {
132
+ public async send(ctx: Context, message: Message) {
130
133
  if (this._ready.state !== TriggerState.RESOLVED) {
131
134
  log('waiting for websocket');
132
135
  await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
@@ -143,6 +146,15 @@ export class EdgeClient extends Resource implements EdgeConnection {
143
146
  throw new EdgeIdentityChangedError();
144
147
  }
145
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
+
146
158
  this._currentConnection.send(message);
147
159
  }
148
160
 
@@ -151,7 +163,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
151
163
  return () => this._messageListeners.delete(listener);
152
164
  }
153
165
 
154
- public onReconnected(listener: () => void) {
166
+ public onReconnected(listener: ReconnectListener) {
155
167
  this._reconnectListeners.add(listener);
156
168
  if (this._ready.state === TriggerState.RESOLVED) {
157
169
  // Microtask so that listener is always called asynchronously, no matter the state of the ready trigger
@@ -219,7 +231,11 @@ export class EdgeClient extends Resource implements EdgeConnection {
219
231
  log('Opening websocket', { url: url.toString(), protocolHeader });
220
232
  const connection = new EdgeWsConnection(
221
233
  identity,
222
- { url, protocolHeader },
234
+ {
235
+ url,
236
+ protocolHeader,
237
+ headers: this._config.clientTag ? { 'X-DXOS-Client-Tag': this._config.clientTag } : undefined,
238
+ },
223
239
  {
224
240
  onConnected: () => {
225
241
  if (this._isActive(connection)) {
@@ -253,9 +269,23 @@ export class EdgeClient extends Resource implements EdgeConnection {
253
269
  this._currentConnection = connection;
254
270
 
255
271
  await connection.open();
256
- // Race with restartRequired so that restart is not blocked by _connect execution.
257
- // Wait on ready to attempt a reconnect if it times out.
258
- 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
+ }
288
+
259
289
  return connection;
260
290
  }
261
291
 
@@ -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';
@@ -16,7 +16,41 @@ describe.skipIf(process.env.CI)('EdgeHttpClient', () => {
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
+ });