@dxos/edge-client 0.6.13 → 0.6.14-main.1366248
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-ZWJXA37R.mjs +113 -0
- package/dist/lib/browser/chunk-ZWJXA37R.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +802 -286
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +128 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/chunk-ANV2HBEH.cjs +136 -0
- package/dist/lib/node/chunk-ANV2HBEH.cjs.map +7 -0
- package/dist/lib/node/index.cjs +798 -283
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +158 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/lib/node-esm/chunk-HNVT57AU.mjs +115 -0
- package/dist/lib/node-esm/chunk-HNVT57AU.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +994 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +129 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/auth.d.ts +22 -0
- package/dist/types/src/auth.d.ts.map +1 -0
- package/dist/types/src/defs.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +30 -26
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +48 -0
- package/dist/types/src/edge-http-client.d.ts.map +1 -0
- package/dist/types/src/edge-identity.d.ts +15 -0
- package/dist/types/src/edge-identity.d.ts.map +1 -0
- package/dist/types/src/edge-ws-connection.d.ts +30 -0
- package/dist/types/src/edge-ws-connection.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +4 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/persistent-lifecycle.d.ts +7 -5
- package/dist/types/src/persistent-lifecycle.d.ts.map +1 -1
- package/dist/types/src/protocol.d.ts +2 -2
- package/dist/types/src/protocol.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/test-utils.d.ts +22 -0
- package/dist/types/src/testing/test-utils.d.ts.map +1 -0
- package/dist/types/src/utils.d.ts +2 -0
- package/dist/types/src/utils.d.ts.map +1 -0
- package/package.json +27 -17
- package/src/auth.ts +135 -0
- package/src/defs.ts +2 -3
- package/src/edge-client.test.ts +144 -25
- package/src/edge-client.ts +181 -127
- package/src/edge-http-client.ts +213 -0
- package/src/edge-identity.ts +31 -0
- package/src/edge-ws-connection.ts +148 -0
- package/src/errors.ts +8 -2
- package/src/index.ts +4 -0
- package/src/persistent-lifecycle.test.ts +2 -2
- package/src/persistent-lifecycle.ts +26 -11
- package/src/protocol.test.ts +1 -2
- package/src/protocol.ts +2 -2
- package/src/testing/index.ts +5 -0
- package/src/testing/test-utils.ts +117 -0
- package/src/utils.ts +10 -0
- package/src/websocket.test.ts +5 -4
- package/dist/types/src/test-utils.d.ts +0 -11
- package/dist/types/src/test-utils.d.ts.map +0 -1
- package/src/test-utils.ts +0 -49
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import WebSocket from 'isomorphic-ws';
|
|
6
|
+
|
|
7
|
+
import { scheduleTask, scheduleTaskInterval } from '@dxos/async';
|
|
8
|
+
import { Context, Resource } from '@dxos/context';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { log, logInfo } from '@dxos/log';
|
|
11
|
+
import { buf } from '@dxos/protocols/buf';
|
|
12
|
+
import { MessageSchema, type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
13
|
+
|
|
14
|
+
import { protocol } from './defs';
|
|
15
|
+
import { type EdgeIdentity } from './edge-identity';
|
|
16
|
+
import { toUint8Array } from './protocol';
|
|
17
|
+
|
|
18
|
+
const SIGNAL_KEEPALIVE_INTERVAL = 5_000;
|
|
19
|
+
|
|
20
|
+
export type EdgeWsConnectionCallbacks = {
|
|
21
|
+
onConnected: () => void;
|
|
22
|
+
onMessage: (message: Message) => void;
|
|
23
|
+
onRestartRequired: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class EdgeWsConnection extends Resource {
|
|
27
|
+
private _inactivityTimeoutCtx: Context | undefined;
|
|
28
|
+
private _ws: WebSocket | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly _identity: EdgeIdentity,
|
|
32
|
+
private readonly _connectionInfo: { url: URL; protocolHeader?: string },
|
|
33
|
+
private readonly _callbacks: EdgeWsConnectionCallbacks,
|
|
34
|
+
) {
|
|
35
|
+
super();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@logInfo
|
|
39
|
+
public get info() {
|
|
40
|
+
return {
|
|
41
|
+
open: this.isOpen,
|
|
42
|
+
identity: this._identity.identityKey,
|
|
43
|
+
device: this._identity.peerKey,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public send(message: Message) {
|
|
48
|
+
invariant(this._ws);
|
|
49
|
+
log('sending...', { peerKey: this._identity.peerKey, payload: protocol.getPayloadType(message) });
|
|
50
|
+
this._ws.send(buf.toBinary(MessageSchema, message));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected override async _open() {
|
|
54
|
+
this._ws = new WebSocket(
|
|
55
|
+
this._connectionInfo.url,
|
|
56
|
+
this._connectionInfo.protocolHeader ? [this._connectionInfo.protocolHeader] : [],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this._ws.onopen = () => {
|
|
60
|
+
if (this.isOpen) {
|
|
61
|
+
log('connected');
|
|
62
|
+
this._callbacks.onConnected();
|
|
63
|
+
this._scheduleHeartbeats();
|
|
64
|
+
} else {
|
|
65
|
+
log.verbose('connected after becoming inactive', { currentIdentity: this._identity });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this._ws.onclose = () => {
|
|
69
|
+
if (this.isOpen) {
|
|
70
|
+
log('disconnected while being open');
|
|
71
|
+
this._callbacks.onRestartRequired();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
this._ws.onerror = (event) => {
|
|
75
|
+
if (this.isOpen) {
|
|
76
|
+
log.warn('edge connection socket error', { error: event.error, info: event.message });
|
|
77
|
+
this._callbacks.onRestartRequired();
|
|
78
|
+
} else {
|
|
79
|
+
log.verbose('error ignored on closed connection', { error: event.error });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
|
84
|
+
*/
|
|
85
|
+
this._ws.onmessage = async (event) => {
|
|
86
|
+
if (!this.isOpen) {
|
|
87
|
+
log.verbose('message ignored on closed connection', { event: event.type });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event.data === '__pong__') {
|
|
91
|
+
this._rescheduleHeartbeatTimeout();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const data = await toUint8Array(event.data);
|
|
95
|
+
if (this.isOpen) {
|
|
96
|
+
const message = buf.fromBinary(MessageSchema, data);
|
|
97
|
+
log('received', { from: message.source, payload: protocol.getPayloadType(message) });
|
|
98
|
+
this._callbacks.onMessage(message);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected override async _close() {
|
|
104
|
+
void this._inactivityTimeoutCtx?.dispose().catch(() => {});
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
this._ws?.close();
|
|
108
|
+
this._ws = undefined;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof Error && err.message.includes('WebSocket is closed before the connection is established.')) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
log.warn('Error closing websocket', { err });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _scheduleHeartbeats() {
|
|
118
|
+
invariant(this._ws);
|
|
119
|
+
scheduleTaskInterval(
|
|
120
|
+
this._ctx,
|
|
121
|
+
async () => {
|
|
122
|
+
// TODO(mykola): use RFC6455 ping/pong once implemented in the browser?
|
|
123
|
+
// Cloudflare's worker responds to this `without interrupting hibernation`. https://developers.cloudflare.com/durable-objects/api/websockets/#setwebsocketautoresponse
|
|
124
|
+
this._ws?.send('__ping__');
|
|
125
|
+
},
|
|
126
|
+
SIGNAL_KEEPALIVE_INTERVAL,
|
|
127
|
+
);
|
|
128
|
+
this._ws.send('__ping__');
|
|
129
|
+
this._rescheduleHeartbeatTimeout();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _rescheduleHeartbeatTimeout() {
|
|
133
|
+
if (!this.isOpen) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
void this._inactivityTimeoutCtx?.dispose();
|
|
137
|
+
this._inactivityTimeoutCtx = new Context();
|
|
138
|
+
scheduleTask(
|
|
139
|
+
this._inactivityTimeoutCtx,
|
|
140
|
+
() => {
|
|
141
|
+
if (this.isOpen) {
|
|
142
|
+
this._callbacks.onRestartRequired();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
2 * SIGNAL_KEEPALIVE_INTERVAL,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class EdgeConnectionClosedError extends Error {
|
|
6
6
|
constructor() {
|
|
7
|
-
super('
|
|
7
|
+
super('Edge connection closed.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EdgeIdentityChangedError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super('Edge identity changed.');
|
|
8
14
|
}
|
|
9
15
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,3 +7,7 @@ export * from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
|
7
7
|
export * from './edge-client';
|
|
8
8
|
export * from './defs';
|
|
9
9
|
export * from './protocol';
|
|
10
|
+
export * from './errors';
|
|
11
|
+
export * from './auth';
|
|
12
|
+
export * from './edge-http-client';
|
|
13
|
+
export * from './edge-identity';
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { sleep, Trigger } from '@dxos/async';
|
|
8
8
|
import { log } from '@dxos/log';
|
|
9
|
-
import {
|
|
9
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
10
10
|
|
|
11
11
|
import { PersistentLifecycle } from './persistent-lifecycle';
|
|
12
12
|
|
|
@@ -10,17 +10,17 @@ import { log } from '@dxos/log';
|
|
|
10
10
|
const INIT_RESTART_DELAY = 100;
|
|
11
11
|
const DEFAULT_MAX_RESTART_DELAY = 5000;
|
|
12
12
|
|
|
13
|
-
export type PersistentLifecycleParams = {
|
|
13
|
+
export type PersistentLifecycleParams<T> = {
|
|
14
14
|
/**
|
|
15
15
|
* Create connection.
|
|
16
16
|
* If promise resolves successfully, connection is considered established.
|
|
17
17
|
*/
|
|
18
|
-
start: () => Promise<
|
|
18
|
+
start: () => Promise<T | undefined>;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Reset connection to initial state.
|
|
22
22
|
*/
|
|
23
|
-
stop: () => Promise<void>;
|
|
23
|
+
stop: (state: T) => Promise<void>;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Called after successful start.
|
|
@@ -38,16 +38,17 @@ export type PersistentLifecycleParams = {
|
|
|
38
38
|
* Handles restarts (e.g. persists connection).
|
|
39
39
|
* Restarts are scheduled with exponential backoff.
|
|
40
40
|
*/
|
|
41
|
-
export class PersistentLifecycle extends Resource {
|
|
42
|
-
private readonly _start: () => Promise<
|
|
43
|
-
private readonly _stop: () => Promise<void>;
|
|
41
|
+
export class PersistentLifecycle<T> extends Resource {
|
|
42
|
+
private readonly _start: () => Promise<T | undefined>;
|
|
43
|
+
private readonly _stop: (state: T) => Promise<void>;
|
|
44
44
|
private readonly _onRestart?: () => Promise<void>;
|
|
45
45
|
private readonly _maxRestartDelay: number;
|
|
46
46
|
|
|
47
|
+
private _currentContext: T | undefined = undefined;
|
|
47
48
|
private _restartTask?: DeferredTask = undefined;
|
|
48
49
|
private _restartAfter = 0;
|
|
49
50
|
|
|
50
|
-
constructor({ start, stop, onRestart, maxRestartDelay = DEFAULT_MAX_RESTART_DELAY }: PersistentLifecycleParams) {
|
|
51
|
+
constructor({ start, stop, onRestart, maxRestartDelay = DEFAULT_MAX_RESTART_DELAY }: PersistentLifecycleParams<T>) {
|
|
51
52
|
super();
|
|
52
53
|
this._start = start;
|
|
53
54
|
this._stop = stop;
|
|
@@ -65,21 +66,22 @@ export class PersistentLifecycle extends Resource {
|
|
|
65
66
|
this._restartTask?.schedule();
|
|
66
67
|
}
|
|
67
68
|
});
|
|
68
|
-
await this._start().catch((err) => {
|
|
69
|
+
this._currentContext = await this._start().catch((err) => {
|
|
69
70
|
log.warn('Start failed', { err });
|
|
70
71
|
this._restartTask?.schedule();
|
|
72
|
+
return undefined;
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
protected override async _close() {
|
|
75
77
|
await this._restartTask?.join();
|
|
76
|
-
await this.
|
|
78
|
+
await this._stopCurrentContext();
|
|
77
79
|
this._restartTask = undefined;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
private async _restart() {
|
|
81
83
|
log(`restarting in ${this._restartAfter}ms`, { state: this._lifecycleState });
|
|
82
|
-
await this.
|
|
84
|
+
await this._stopCurrentContext();
|
|
83
85
|
if (this._lifecycleState !== LifecycleState.OPEN) {
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
@@ -87,12 +89,25 @@ export class PersistentLifecycle extends Resource {
|
|
|
87
89
|
this._restartAfter = Math.min(Math.max(this._restartAfter * 2, INIT_RESTART_DELAY), this._maxRestartDelay);
|
|
88
90
|
|
|
89
91
|
// May fail if the connection is not established.
|
|
90
|
-
await warnAfterTimeout(5_000, 'Connection establishment takes too long', () =>
|
|
92
|
+
await warnAfterTimeout(5_000, 'Connection establishment takes too long', async () => {
|
|
93
|
+
this._currentContext = await this._start();
|
|
94
|
+
});
|
|
91
95
|
|
|
92
96
|
this._restartAfter = 0;
|
|
93
97
|
await this._onRestart?.();
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
private async _stopCurrentContext() {
|
|
101
|
+
if (this._currentContext) {
|
|
102
|
+
try {
|
|
103
|
+
await this._stop(this._currentContext);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log.catch(err);
|
|
106
|
+
}
|
|
107
|
+
this._currentContext = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
/**
|
|
97
112
|
* Scheduling restart should be done from outside.
|
|
98
113
|
*/
|
package/src/protocol.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { buf } from '@dxos/protocols/buf';
|
|
8
8
|
import {
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
SwarmRequestSchema,
|
|
12
12
|
SwarmResponseSchema,
|
|
13
13
|
} from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
14
|
-
import { describe, test } from '@dxos/test';
|
|
15
14
|
|
|
16
15
|
import { Protocol } from './protocol';
|
|
17
16
|
|
package/src/protocol.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { invariant } from '@dxos/invariant';
|
|
6
6
|
import { buf, bufWkt } from '@dxos/protocols/buf';
|
|
7
|
-
import { type Message, MessageSchema, type
|
|
7
|
+
import { type Message, MessageSchema, type PeerSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
8
8
|
import { bufferToArray } from '@dxos/util';
|
|
9
9
|
|
|
10
|
-
export type PeerData =
|
|
10
|
+
export type PeerData = buf.MessageInitShape<typeof PeerSchema>;
|
|
11
11
|
|
|
12
12
|
export const getTypename = (typeName: string) => `type.googleapis.com/${typeName}`;
|
|
13
13
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import WebSocket from 'isomorphic-ws';
|
|
6
|
+
|
|
7
|
+
import { Trigger } from '@dxos/async';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { buf } from '@dxos/protocols/buf';
|
|
10
|
+
import { MessageSchema, TextMessageSchema, type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
11
|
+
|
|
12
|
+
import { protocol } from '../defs';
|
|
13
|
+
import { toUint8Array } from '../protocol';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_PORT = 8080;
|
|
16
|
+
|
|
17
|
+
type TestEdgeWsServerParams = {
|
|
18
|
+
admitConnection?: Trigger;
|
|
19
|
+
payloadDecoder?: (payload: Uint8Array) => any;
|
|
20
|
+
messageHandler?: (payload: any) => Promise<Uint8Array | undefined>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const createTestEdgeWsServer = async (port = DEFAULT_PORT, params?: TestEdgeWsServerParams) => {
|
|
24
|
+
const server = new WebSocket.Server({ port, verifyClient: createConnectionDelayHandler(params) });
|
|
25
|
+
|
|
26
|
+
let connection: WebSocket | undefined;
|
|
27
|
+
|
|
28
|
+
const messageSink: any[] = [];
|
|
29
|
+
const messageSourceLog: any[] = [];
|
|
30
|
+
const closeTrigger = new Trigger();
|
|
31
|
+
const sendResponseMessage = createResponseSender(() => connection!);
|
|
32
|
+
|
|
33
|
+
server.on('connection', (ws) => {
|
|
34
|
+
connection = ws;
|
|
35
|
+
ws.on('error', (err) => log.catch(err));
|
|
36
|
+
ws.on('message', async (data) => {
|
|
37
|
+
if (String(data) === '__ping__') {
|
|
38
|
+
ws.send('__pong__');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { request, requestPayload } = await decodeRequest(params, data);
|
|
42
|
+
messageSourceLog.push(request.source);
|
|
43
|
+
if (params?.messageHandler) {
|
|
44
|
+
const responsePayload = await params.messageHandler(requestPayload);
|
|
45
|
+
if (responsePayload && connection) {
|
|
46
|
+
sendResponseMessage(request, responsePayload);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
log('message', { payload: requestPayload });
|
|
50
|
+
messageSink.push(requestPayload);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ws.on('close', () => {
|
|
54
|
+
connection = undefined;
|
|
55
|
+
closeTrigger.wake();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
server,
|
|
61
|
+
messageSink,
|
|
62
|
+
messageSourceLog,
|
|
63
|
+
endpoint: `ws://localhost:${port}`,
|
|
64
|
+
cleanup: () => server.close(),
|
|
65
|
+
currentConnection: () => connection,
|
|
66
|
+
sendResponseMessage,
|
|
67
|
+
sendMessage: (msg: Message) => {
|
|
68
|
+
connection!.send(buf.toBinary(MessageSchema, msg));
|
|
69
|
+
},
|
|
70
|
+
closeConnection: () => {
|
|
71
|
+
closeTrigger.reset();
|
|
72
|
+
connection!.close(1011);
|
|
73
|
+
return closeTrigger.wait();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const createConnectionDelayHandler = (params: TestEdgeWsServerParams | undefined) => {
|
|
79
|
+
return (_: any, callback: (admit: boolean) => void) => {
|
|
80
|
+
if (params?.admitConnection) {
|
|
81
|
+
log('delaying edge connection admission');
|
|
82
|
+
void params.admitConnection.wait().then(() => {
|
|
83
|
+
callback(true);
|
|
84
|
+
log('edge connection admitted');
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
callback(true);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const createResponseSender = (connection: () => WebSocket) => {
|
|
93
|
+
return (request: Message, responsePayload: Uint8Array) => {
|
|
94
|
+
const recipient = request.source!;
|
|
95
|
+
connection().send(
|
|
96
|
+
buf.toBinary(
|
|
97
|
+
MessageSchema,
|
|
98
|
+
buf.create(MessageSchema, {
|
|
99
|
+
source: {
|
|
100
|
+
identityKey: recipient.identityKey,
|
|
101
|
+
peerKey: recipient.peerKey,
|
|
102
|
+
},
|
|
103
|
+
serviceId: request.serviceId!,
|
|
104
|
+
payload: { value: responsePayload },
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const decodeRequest = async (params: TestEdgeWsServerParams | undefined, data: any) => {
|
|
112
|
+
const request = buf.fromBinary(MessageSchema, await toUint8Array(data));
|
|
113
|
+
const requestPayload = params?.payloadDecoder
|
|
114
|
+
? params.payloadDecoder(request.payload!.value!)
|
|
115
|
+
: protocol.getPayload(request, TextMessageSchema);
|
|
116
|
+
return { request, requestPayload };
|
|
117
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export const getEdgeUrlWithProtocol = (baseUrl: string, protocol: 'http' | 'ws') => {
|
|
6
|
+
const isSecure = baseUrl.startsWith('https') || baseUrl.startsWith('wss');
|
|
7
|
+
const url = new URL(baseUrl);
|
|
8
|
+
url.protocol = protocol + (isSecure ? 's' : '');
|
|
9
|
+
return url.toString();
|
|
10
|
+
};
|
package/src/websocket.test.ts
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from 'chai';
|
|
6
5
|
import WebSocket from 'isomorphic-ws';
|
|
6
|
+
import { describe, expect, test, onTestFinished } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import { Trigger, TriggerState } from '@dxos/async';
|
|
9
|
-
import { describe, test } from '@dxos/test';
|
|
10
9
|
|
|
11
|
-
import {
|
|
10
|
+
import { createTestEdgeWsServer } from './testing';
|
|
12
11
|
|
|
13
12
|
describe('WebSocket', () => {
|
|
14
13
|
test('swap `onclose` handler ', async () => {
|
|
15
|
-
const { endpoint } = await
|
|
14
|
+
const { endpoint, cleanup } = await createTestEdgeWsServer(8003);
|
|
15
|
+
onTestFinished(cleanup);
|
|
16
|
+
|
|
16
17
|
const ws = new WebSocket(endpoint);
|
|
17
18
|
const opened = new Trigger();
|
|
18
19
|
ws.onopen = () => {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'isomorphic-ws';
|
|
2
|
-
export declare const DEFAULT_PORT = 8080;
|
|
3
|
-
export declare const createTestWsServer: (port?: number) => Promise<{
|
|
4
|
-
server: WebSocket.Server;
|
|
5
|
-
/**
|
|
6
|
-
* Close the server connection.
|
|
7
|
-
*/
|
|
8
|
-
error: () => Promise<void>;
|
|
9
|
-
endpoint: string;
|
|
10
|
-
}>;
|
|
11
|
-
//# sourceMappingURL=test-utils.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../src/test-utils.ts"],"names":[],"mappings":"AAIA,OAAO,SAAS,MAAM,eAAe,CAAC;AAWtC,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,eAAO,MAAM,kBAAkB;;IAsB3B;;OAEG;;;EAON,CAAC"}
|
package/src/test-utils.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import WebSocket from 'isomorphic-ws';
|
|
6
|
-
|
|
7
|
-
import { Trigger } from '@dxos/async';
|
|
8
|
-
import { log } from '@dxos/log';
|
|
9
|
-
import { buf } from '@dxos/protocols/buf';
|
|
10
|
-
import { MessageSchema, TextMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
11
|
-
import { afterTest } from '@dxos/test';
|
|
12
|
-
|
|
13
|
-
import { protocol } from './defs';
|
|
14
|
-
import { toUint8Array } from './protocol';
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_PORT = 8080;
|
|
17
|
-
|
|
18
|
-
export const createTestWsServer = async (port = DEFAULT_PORT) => {
|
|
19
|
-
const server = new WebSocket.Server({ port });
|
|
20
|
-
let connection: WebSocket;
|
|
21
|
-
const closeTrigger = new Trigger();
|
|
22
|
-
server.on('connection', (ws) => {
|
|
23
|
-
connection = ws;
|
|
24
|
-
ws.on('error', (err) => log.catch(err));
|
|
25
|
-
ws.on('message', async (data) => {
|
|
26
|
-
if (String(data) === '__ping__') {
|
|
27
|
-
ws.send('__pong__');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
log('message', {
|
|
31
|
-
payload: protocol.getPayload(buf.fromBinary(MessageSchema, await toUint8Array(data)), TextMessageSchema),
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
ws.on('close', () => closeTrigger.wake());
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterTest(() => server.close());
|
|
38
|
-
return {
|
|
39
|
-
server,
|
|
40
|
-
/**
|
|
41
|
-
* Close the server connection.
|
|
42
|
-
*/
|
|
43
|
-
error: async () => {
|
|
44
|
-
connection.close(1011);
|
|
45
|
-
await closeTrigger.wait();
|
|
46
|
-
},
|
|
47
|
-
endpoint: `ws://localhost:${port}`,
|
|
48
|
-
};
|
|
49
|
-
};
|