@dxos/edge-client 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea

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 (33) hide show
  1. package/dist/lib/browser/{chunk-IKP53CBQ.mjs → chunk-VESGVCLQ.mjs} +15 -44
  2. package/dist/lib/browser/{chunk-IKP53CBQ.mjs.map → chunk-VESGVCLQ.mjs.map} +2 -2
  3. package/dist/lib/browser/edge-ws-muxer.mjs +1 -1
  4. package/dist/lib/browser/index.mjs +247 -156
  5. package/dist/lib/browser/index.mjs.map +3 -3
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/testing/index.mjs +1 -1
  8. package/dist/lib/browser/testing/index.mjs.map +2 -2
  9. package/dist/lib/node-esm/{chunk-DR5YNW5K.mjs → chunk-JTBFRYNM.mjs} +15 -44
  10. package/dist/lib/node-esm/{chunk-DR5YNW5K.mjs.map → chunk-JTBFRYNM.mjs.map} +2 -2
  11. package/dist/lib/node-esm/edge-ws-muxer.mjs +1 -1
  12. package/dist/lib/node-esm/index.mjs +247 -156
  13. package/dist/lib/node-esm/index.mjs.map +3 -3
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/testing/index.mjs +1 -1
  16. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  17. package/dist/types/src/edge-client.d.ts.map +1 -1
  18. package/dist/types/src/edge-http-client.d.ts +1 -0
  19. package/dist/types/src/edge-http-client.d.ts.map +1 -1
  20. package/dist/types/src/edge-ws-connection.d.ts +19 -0
  21. package/dist/types/src/edge-ws-connection.d.ts.map +1 -1
  22. package/dist/types/src/http-client.d.ts +10 -7
  23. package/dist/types/src/http-client.d.ts.map +1 -1
  24. package/dist/types/src/testing/test-utils.d.ts +1 -1
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +14 -14
  27. package/src/edge-client.test.ts +4 -4
  28. package/src/edge-client.ts +36 -5
  29. package/src/edge-http-client.ts +24 -20
  30. package/src/edge-ws-connection.ts +118 -5
  31. package/src/http-client.test.ts +6 -4
  32. package/src/http-client.ts +13 -7
  33. package/src/testing/test-utils.ts +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/edge-client",
3
- "version": "0.8.4-main.a4bbb77",
3
+ "version": "0.8.4-main.ae835ea",
4
4
  "description": "EDGE Client",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -45,21 +45,21 @@
45
45
  "@effect/platform": "^0.92.1",
46
46
  "isomorphic-ws": "^5.0.0",
47
47
  "ws": "^8.14.2",
48
- "@dxos/async": "0.8.4-main.a4bbb77",
49
- "@dxos/context": "0.8.4-main.a4bbb77",
50
- "@dxos/credentials": "0.8.4-main.a4bbb77",
51
- "@dxos/crypto": "0.8.4-main.a4bbb77",
52
- "@dxos/debug": "0.8.4-main.a4bbb77",
53
- "@dxos/invariant": "0.8.4-main.a4bbb77",
54
- "@dxos/log": "0.8.4-main.a4bbb77",
55
- "@dxos/keyring": "0.8.4-main.a4bbb77",
56
- "@dxos/keys": "0.8.4-main.a4bbb77",
57
- "@dxos/node-std": "0.8.4-main.a4bbb77",
58
- "@dxos/protocols": "0.8.4-main.a4bbb77",
59
- "@dxos/util": "0.8.4-main.a4bbb77"
48
+ "@dxos/credentials": "0.8.4-main.ae835ea",
49
+ "@dxos/context": "0.8.4-main.ae835ea",
50
+ "@dxos/debug": "0.8.4-main.ae835ea",
51
+ "@dxos/invariant": "0.8.4-main.ae835ea",
52
+ "@dxos/keyring": "0.8.4-main.ae835ea",
53
+ "@dxos/keys": "0.8.4-main.ae835ea",
54
+ "@dxos/log": "0.8.4-main.ae835ea",
55
+ "@dxos/async": "0.8.4-main.ae835ea",
56
+ "@dxos/node-std": "0.8.4-main.ae835ea",
57
+ "@dxos/crypto": "0.8.4-main.ae835ea",
58
+ "@dxos/protocols": "0.8.4-main.ae835ea",
59
+ "@dxos/util": "0.8.4-main.ae835ea"
60
60
  },
61
61
  "devDependencies": {
62
- "@dxos/test-utils": "0.8.4-main.a4bbb77"
62
+ "@dxos/test-utils": "0.8.4-main.ae835ea"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "effect": "^3.13.3"
@@ -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 () => {
@@ -2,7 +2,14 @@
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';
6
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';
@@ -17,6 +24,9 @@ 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
 
@@ -80,10 +90,19 @@ export class EdgeClient extends Resource implements EdgeConnection {
80
90
  };
81
91
  }
82
92
 
83
- get status() {
84
- return Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED
85
- ? EdgeStatus.CONNECTED
86
- : EdgeStatus.NOT_CONNECTED;
93
+ get status(): EdgeStatus {
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() {
@@ -159,6 +178,18 @@ export class EdgeClient extends Resource implements EdgeConnection {
159
178
  this._persistentLifecycle.open().catch((err) => {
160
179
  log.warn('Error while opening connection', { err });
161
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
+ );
162
193
  }
163
194
 
164
195
  /**
@@ -2,11 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FetchHttpClient, HttpClient } from '@effect/platform';
6
- import { Effect, pipe } from 'effect';
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 {
@@ -15,8 +18,8 @@ import {
15
18
  type CreateSpaceRequest,
16
19
  type CreateSpaceResponseBody,
17
20
  EdgeAuthChallengeError,
21
+ type EdgeBody,
18
22
  EdgeCallFailedError,
19
- type EdgeHttpResponse,
20
23
  type EdgeStatus,
21
24
  type ExecuteWorkflowResponseBody,
22
25
  type ExportBundleRequest,
@@ -75,6 +78,7 @@ type EdgeHttpRequestArgs = {
75
78
 
76
79
  /**
77
80
  * Do not expect a standard EDGE JSON response with a `success` field.
81
+ * @deprecated Use only for debugging.
78
82
  */
79
83
  rawResponse?: boolean;
80
84
  };
@@ -372,7 +376,7 @@ export class EdgeHttpClient {
372
376
  //
373
377
 
374
378
  private async _fetch<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
375
- return pipe(
379
+ return Function.pipe(
376
380
  HttpClient.get(url),
377
381
  withLogging,
378
382
  withRetryConfig,
@@ -386,50 +390,50 @@ export class EdgeHttpClient {
386
390
  // TODO(burdon): Refactor with effect (see edge-http-client.test.ts).
387
391
  private async _call<T>(url: URL, args: EdgeHttpRequestArgs): Promise<T> {
388
392
  const shouldRetry = createRetryHandler(args);
389
- const requestContext = args.context ?? new Context();
393
+ const requestContext = args.context ?? Context.default();
390
394
  log('fetch', { url, request: args.body });
391
395
 
392
396
  let handledAuth = false;
393
397
  while (true) {
394
398
  let processingError: EdgeCallFailedError | undefined = undefined;
395
- let retryAfterHeaderValue: number = Number.NaN;
396
399
  try {
397
400
  const request = createRequest(args, this._authHeader);
398
401
  const response = await fetch(url, request);
399
- retryAfterHeaderValue = Number(response.headers.get('Retry-After'));
400
- if (response.ok) {
401
- const body = (await response.json()) as EdgeHttpResponse<T>;
402
+ const body: EdgeBody<T> | undefined =
403
+ response.headers.get('Content-Type') === 'application/json' ? await response.clone().json() : undefined;
402
404
 
405
+ if (response.ok) {
403
406
  if (args.rawResponse) {
404
407
  return body as any;
405
408
  }
406
-
409
+ invariant(body, 'Expected body to be present');
407
410
  if (!('success' in body)) {
408
411
  return body;
409
412
  }
410
-
411
413
  if (body.success) {
412
414
  return body.data;
413
415
  }
414
-
415
- log.warn('unsuccessful edge response', { url, body });
416
- if (body.errorData?.type === 'auth_challenge' && typeof body.errorData?.challenge === 'string') {
417
- processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
418
- } else if (body.errorData) {
419
- processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
420
- }
421
416
  } else if (response.status === 401 && !handledAuth) {
422
417
  this._authHeader = await this._handleUnauthorized(response);
423
418
  handledAuth = true;
424
419
  continue;
420
+ }
421
+
422
+ invariant(!body?.success, 'Expected body to not be a failure response or undefined.');
423
+
424
+ if (body?.errorData?.type === 'auth_challenge' && typeof body?.errorData?.challenge === 'string') {
425
+ processingError = new EdgeAuthChallengeError(body.errorData.challenge, body.errorData);
426
+ } else if (body?.success === false) {
427
+ processingError = EdgeCallFailedError.fromUnsuccessfulResponse(response, body);
425
428
  } else {
429
+ invariant(!response.ok, 'Expected response to not be ok.');
426
430
  processingError = await EdgeCallFailedError.fromHttpFailure(response);
427
431
  }
428
432
  } catch (error: any) {
429
433
  processingError = EdgeCallFailedError.fromProcessingFailureCause(error);
430
434
  }
431
435
 
432
- if (processingError?.isRetryable && (await shouldRetry(requestContext, retryAfterHeaderValue))) {
436
+ if (processingError?.isRetryable && (await shouldRetry(requestContext, processingError.retryAfterMs))) {
433
437
  log('retrying edge request', { url, processingError });
434
438
  } else {
435
439
  throw processingError!;
@@ -489,7 +493,7 @@ const createRetryHandler = ({ retry }: EdgeHttpRequestArgs) => {
489
493
  const maxRetries = retry.count ?? DEFAULT_MAX_RETRIES_COUNT;
490
494
  const baseTimeout = retry.timeout ?? DEFAULT_RETRY_TIMEOUT;
491
495
  const jitter = retry.jitter ?? DEFAULT_RETRY_JITTER;
492
- return async (ctx: Context, retryAfter: number) => {
496
+ return async (ctx: Context, retryAfter?: number) => {
493
497
  if (++retries > maxRetries || ctx.disposed) {
494
498
  return false;
495
499
  }
@@ -32,6 +32,22 @@ export class EdgeWsConnection extends Resource {
32
32
  private _wsMuxer: WebSocketMuxer | undefined;
33
33
  private _lastReceivedMessageTimestamp = Date.now();
34
34
 
35
+ private _openTimestamp: number | undefined;
36
+
37
+ // Latency tracking.
38
+ private _pingTimestamp: number | undefined;
39
+ private _rtt = 0;
40
+
41
+ // Rate tracking with sliding window.
42
+ private _uploadRate = 0;
43
+ private _downloadRate = 0;
44
+ private readonly _rateWindow = 10000; // 10 second sliding window.
45
+ private readonly _rateUpdateInterval = 1000; // Update rates every second.
46
+ private _bytesSamples: Array<{ timestamp: number; sent: number; received: number }> = [];
47
+
48
+ private _messagesSent = 0;
49
+ private _messagesReceived = 0;
50
+
35
51
  constructor(
36
52
  private readonly _identity: EdgeIdentity,
37
53
  private readonly _connectionInfo: { url: URL; protocolHeader?: string },
@@ -49,10 +65,35 @@ export class EdgeWsConnection extends Resource {
49
65
  };
50
66
  }
51
67
 
68
+ public get rtt(): number {
69
+ return this._rtt;
70
+ }
71
+
72
+ public get uptime(): number {
73
+ return this._openTimestamp ? (Date.now() - this._openTimestamp) / 1000 : 0;
74
+ }
75
+
76
+ public get uploadRate(): number {
77
+ return this._uploadRate;
78
+ }
79
+
80
+ public get downloadRate(): number {
81
+ return this._downloadRate;
82
+ }
83
+
84
+ public get messagesSent(): number {
85
+ return this._messagesSent;
86
+ }
87
+
88
+ public get messagesReceived(): number {
89
+ return this._messagesReceived;
90
+ }
91
+
52
92
  public send(message: Message): void {
53
93
  invariant(this._ws);
54
94
  invariant(this._wsMuxer);
55
95
  log('sending...', { peerKey: this._identity.peerKey, payload: protocol.getPayloadType(message) });
96
+ this._messagesSent++;
56
97
  if (this._ws?.protocol.includes(EdgeWebsocketProtocol.V0)) {
57
98
  const binary = buf.toBinary(MessageSchema, message);
58
99
  if (binary.length > CLOUDFLARE_MESSAGE_MAX_BYTES) {
@@ -63,8 +104,12 @@ export class EdgeWsConnection extends Resource {
63
104
  });
64
105
  return;
65
106
  }
107
+ this._recordBytes(binary.byteLength, 0);
66
108
  this._ws.send(binary);
67
109
  } else {
110
+ // For muxer, we need to track the size of the message being sent.
111
+ const binary = buf.toBinary(MessageSchema, message);
112
+ this._recordBytes(binary.byteLength, 0);
68
113
  this._wsMuxer.send(message).catch((e) => log.catch(e));
69
114
  }
70
115
  }
@@ -83,20 +128,22 @@ export class EdgeWsConnection extends Resource {
83
128
  this._ws.onopen = () => {
84
129
  if (this.isOpen) {
85
130
  log('connected');
131
+ this._openTimestamp = Date.now();
86
132
  this._callbacks.onConnected();
87
133
  this._scheduleHeartbeats();
134
+ this._scheduleRateCalculation();
88
135
  } else {
89
136
  log.verbose('connected after becoming inactive', { currentIdentity: this._identity });
90
137
  }
91
138
  };
92
- this._ws.onclose = (event) => {
139
+ this._ws.onclose = (event: WebSocket.CloseEvent) => {
93
140
  if (this.isOpen) {
94
- log.warn('disconnected while being open', { code: event.code, reason: event.reason });
141
+ log.warn('server disconnected', { code: event.code, reason: event.reason });
95
142
  this._callbacks.onRestartRequired();
96
143
  muxer.destroy();
97
144
  }
98
145
  };
99
- this._ws.onerror = (event) => {
146
+ this._ws.onerror = (event: WebSocket.ErrorEvent) => {
100
147
  if (this.isOpen) {
101
148
  log.warn('edge connection socket error', { error: event.error, info: event.message });
102
149
  this._callbacks.onRestartRequired();
@@ -107,21 +154,29 @@ export class EdgeWsConnection extends Resource {
107
154
  /**
108
155
  * https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
109
156
  */
110
- this._ws.onmessage = async (event) => {
157
+ this._ws.onmessage = async (event: WebSocket.MessageEvent) => {
111
158
  if (!this.isOpen) {
112
159
  log.verbose('message ignored on closed connection', { event: event.type });
113
160
  return;
114
161
  }
115
162
  this._lastReceivedMessageTimestamp = Date.now();
116
163
  if (event.data === '__pong__') {
164
+ // Calculate latency.
165
+ if (this._pingTimestamp) {
166
+ this._rtt = Date.now() - this._pingTimestamp;
167
+ this._pingTimestamp = undefined;
168
+ }
117
169
  this._rescheduleHeartbeatTimeout();
118
170
  return;
119
171
  }
120
172
  const bytes = await toUint8Array(event.data);
173
+ this._recordBytes(0, bytes.byteLength);
121
174
  if (!this.isOpen) {
122
175
  return;
123
176
  }
124
177
 
178
+ this._messagesReceived++;
179
+
125
180
  const message = this._ws?.protocol?.includes(EdgeWebsocketProtocol.V0)
126
181
  ? buf.fromBinary(MessageSchema, bytes)
127
182
  : muxer.receiveData(bytes);
@@ -145,7 +200,7 @@ export class EdgeWsConnection extends Resource {
145
200
  if (err instanceof Error && err.message.includes('WebSocket is closed before the connection is established.')) {
146
201
  return;
147
202
  }
148
- log.warn('Error closing websocket', { err });
203
+ log.warn('error closing websocket', { err });
149
204
  }
150
205
  }
151
206
 
@@ -156,10 +211,12 @@ export class EdgeWsConnection extends Resource {
156
211
  async () => {
157
212
  // TODO(mykola): use RFC6455 ping/pong once implemented in the browser?
158
213
  // Cloudflare's worker responds to this `without interrupting hibernation`. https://developers.cloudflare.com/durable-objects/api/websockets/#setwebsocketautoresponse
214
+ this._pingTimestamp = Date.now();
159
215
  this._ws?.send('__ping__');
160
216
  },
161
217
  SIGNAL_KEEPALIVE_INTERVAL,
162
218
  );
219
+ this._pingTimestamp = Date.now();
163
220
  this._ws.send('__ping__');
164
221
  this._rescheduleHeartbeatTimeout();
165
222
  }
@@ -187,4 +244,60 @@ export class EdgeWsConnection extends Resource {
187
244
  SIGNAL_KEEPALIVE_TIMEOUT,
188
245
  );
189
246
  }
247
+
248
+ private _recordBytes(sent: number, received: number): void {
249
+ const now = Date.now();
250
+
251
+ // Find if we have a sample for the current second.
252
+ const currentSecond = Math.floor(now / 1000) * 1000;
253
+ const existingSample = this._bytesSamples.find((s) => Math.floor(s.timestamp / 1000) * 1000 === currentSecond);
254
+
255
+ if (existingSample) {
256
+ existingSample.sent += sent;
257
+ existingSample.received += received;
258
+ } else {
259
+ this._bytesSamples.push({ timestamp: now, sent, received });
260
+ }
261
+ }
262
+
263
+ private _scheduleRateCalculation(): void {
264
+ scheduleTaskInterval(
265
+ this._ctx,
266
+ async () => {
267
+ this._calculateRates();
268
+ },
269
+ this._rateUpdateInterval,
270
+ );
271
+ // Calculate initial rates.
272
+ this._calculateRates();
273
+ }
274
+
275
+ private _calculateRates(): void {
276
+ const now = Date.now();
277
+ const cutoff = now - this._rateWindow;
278
+
279
+ // Remove old samples.
280
+ this._bytesSamples = this._bytesSamples.filter((s) => s.timestamp > cutoff);
281
+
282
+ if (this._bytesSamples.length === 0) {
283
+ this._uploadRate = 0;
284
+ this._downloadRate = 0;
285
+ return;
286
+ }
287
+
288
+ // Calculate total bytes and time span.
289
+ let totalSent = 0;
290
+ let totalReceived = 0;
291
+ const oldestTimestamp = Math.min(...this._bytesSamples.map((s) => s.timestamp));
292
+ const timeSpan = (now - oldestTimestamp) / 1000; // Convert to seconds.
293
+
294
+ for (const sample of this._bytesSamples) {
295
+ totalSent += sample.sent;
296
+ totalReceived += sample.received;
297
+ }
298
+
299
+ // Calculate rates (bytes per second).
300
+ this._uploadRate = timeSpan > 0 ? Math.round(totalSent / timeSpan) : 0;
301
+ this._downloadRate = timeSpan > 0 ? Math.round(totalReceived / timeSpan) : 0;
302
+ }
190
303
  }
@@ -2,8 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { FetchHttpClient, HttpClient } from '@effect/platform';
6
- import { Effect, pipe } from 'effect';
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
  import { afterEach, beforeEach, describe, it } from 'vitest';
8
10
 
9
11
  import { invariant } from '@dxos/invariant';
@@ -30,7 +32,7 @@ describe('HttpClient', () => {
30
32
  invariant(server);
31
33
 
32
34
  {
33
- const result = await pipe(
35
+ const result = await Function.pipe(
34
36
  withRetry(HttpClient.get(server.url)),
35
37
  Effect.provide(FetchHttpClient.layer),
36
38
  Effect.withSpan('EdgeHttpClient'),
@@ -40,7 +42,7 @@ describe('HttpClient', () => {
40
42
  }
41
43
 
42
44
  {
43
- const result = await pipe(
45
+ const result = await Function.pipe(
44
46
  HttpClient.get(server.url),
45
47
  withLogging,
46
48
  withRetryConfig,
@@ -2,10 +2,14 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
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';
5
+ import type * as HttpClient from '@effect/platform/HttpClient';
6
+ import type * as HttpClientError from '@effect/platform/HttpClientError';
7
+ import type * as HttpClientResponse from '@effect/platform/HttpClientResponse';
8
+ import * as Context from 'effect/Context';
9
+ import * as Duration from 'effect/Duration';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Layer from 'effect/Layer';
12
+ import * as Schedule from 'effect/Schedule';
9
13
 
10
14
  import { log } from '@dxos/log';
11
15
 
@@ -28,7 +32,7 @@ export class HttpConfig extends Context.Tag('HttpConfig')<HttpConfig, RetryOptio
28
32
 
29
33
  // HOC pattern.
30
34
  export const withRetry = (
31
- effect: Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>,
35
+ effect: Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError, HttpClient.HttpClient>,
32
36
  {
33
37
  timeout = Duration.millis(1_000),
34
38
  retryBaseDelay = Duration.millis(1_000),
@@ -48,13 +52,15 @@ export const withRetry = (
48
52
  );
49
53
  };
50
54
 
51
- export const withRetryConfig = (effect: Effect.Effect<HttpClientResponse, HttpClientError, HttpClient.HttpClient>) =>
55
+ export const withRetryConfig = (
56
+ effect: Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError, HttpClient.HttpClient>,
57
+ ) =>
52
58
  Effect.gen(function* () {
53
59
  const config = yield* HttpConfig;
54
60
  return yield* withRetry(effect, config);
55
61
  });
56
62
 
57
- export const withLogging = <A extends HttpClientResponse, E, R>(effect: Effect.Effect<A, E, R>) =>
63
+ export const withLogging = <A extends HttpClientResponse.HttpClientResponse, E, R>(effect: Effect.Effect<A, E, R>) =>
58
64
  effect.pipe(Effect.tap((res) => log.info('response', { status: res.status })));
59
65
 
60
66
  /**
@@ -36,11 +36,11 @@ export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestE
36
36
  const closeTrigger = new Trigger();
37
37
  const sendResponseMessage = createResponseSender(() => connection!.muxer);
38
38
 
39
- wsServer.on('connection', (ws) => {
39
+ wsServer.on('connection', (ws: WebSocket) => {
40
40
  const muxer = new WebSocketMuxer(ws);
41
41
  connection = { ws, muxer };
42
- ws.on('error', (err) => log.catch(err));
43
- ws.on('message', async (data) => {
42
+ ws.on('error', (err: Error) => log.catch(err));
43
+ ws.on('message', async (data: any) => {
44
44
  if (String(data) === '__ping__') {
45
45
  ws.send('__pong__');
46
46
  return;