@dxos/edge-client 0.6.14-staging.e15392e → 0.7.0
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/index.mjs +444 -226
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +7 -4
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +441 -225
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +7 -4
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node-esm/index.mjs +444 -226
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +7 -4
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/auth.d.ts +1 -1
- package/dist/types/src/auth.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +21 -18
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts.map +1 -1
- 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/persistent-lifecycle.d.ts +7 -5
- package/dist/types/src/persistent-lifecycle.d.ts.map +1 -1
- package/dist/types/src/testing/test-utils.d.ts +1 -0
- package/dist/types/src/testing/test-utils.d.ts.map +1 -1
- package/package.json +14 -14
- package/src/auth.ts +4 -1
- package/src/edge-client.test.ts +101 -14
- package/src/edge-client.ts +128 -126
- package/src/edge-http-client.ts +4 -1
- package/src/edge-ws-connection.ts +148 -0
- package/src/persistent-lifecycle.ts +26 -11
- package/src/testing/test-utils.ts +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-ws-connection.d.ts","sourceRoot":"","sources":["../../../src/edge-ws-connection.ts"],"names":[],"mappings":"AAOA,OAAO,EAAW,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIlD,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,4CAA4C,CAAC;AAGzF,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKpD,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,QAAQ;IAK1C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN7B,OAAO,CAAC,qBAAqB,CAAsB;IACnD,OAAO,CAAC,GAAG,CAAwB;gBAGhB,SAAS,EAAE,YAAY,EACvB,eAAe,EAAE;QAAE,GAAG,EAAE,GAAG,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,EACtD,UAAU,EAAE,yBAAyB;IAKxD,IACW,IAAI;;;;MAMd;IAEM,IAAI,CAAC,OAAO,EAAE,OAAO;cAMH,KAAK;cAkDL,MAAM;IAc/B,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,2BAA2B;CAgBpC"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Resource } from '@dxos/context';
|
|
2
|
-
export type PersistentLifecycleParams = {
|
|
2
|
+
export type PersistentLifecycleParams<T> = {
|
|
3
3
|
/**
|
|
4
4
|
* Create connection.
|
|
5
5
|
* If promise resolves successfully, connection is considered established.
|
|
6
6
|
*/
|
|
7
|
-
start: () => Promise<
|
|
7
|
+
start: () => Promise<T | undefined>;
|
|
8
8
|
/**
|
|
9
9
|
* Reset connection to initial state.
|
|
10
10
|
*/
|
|
11
|
-
stop: () => Promise<void>;
|
|
11
|
+
stop: (state: T) => Promise<void>;
|
|
12
12
|
/**
|
|
13
13
|
* Called after successful start.
|
|
14
14
|
*/
|
|
@@ -23,17 +23,19 @@ export type PersistentLifecycleParams = {
|
|
|
23
23
|
* Handles restarts (e.g. persists connection).
|
|
24
24
|
* Restarts are scheduled with exponential backoff.
|
|
25
25
|
*/
|
|
26
|
-
export declare class PersistentLifecycle extends Resource {
|
|
26
|
+
export declare class PersistentLifecycle<T> extends Resource {
|
|
27
27
|
private readonly _start;
|
|
28
28
|
private readonly _stop;
|
|
29
29
|
private readonly _onRestart?;
|
|
30
30
|
private readonly _maxRestartDelay;
|
|
31
|
+
private _currentContext;
|
|
31
32
|
private _restartTask?;
|
|
32
33
|
private _restartAfter;
|
|
33
|
-
constructor({ start, stop, onRestart, maxRestartDelay }: PersistentLifecycleParams);
|
|
34
|
+
constructor({ start, stop, onRestart, maxRestartDelay }: PersistentLifecycleParams<T>);
|
|
34
35
|
protected _open(): Promise<void>;
|
|
35
36
|
protected _close(): Promise<void>;
|
|
36
37
|
private _restart;
|
|
38
|
+
private _stopCurrentContext;
|
|
37
39
|
/**
|
|
38
40
|
* Scheduling restart should be done from outside.
|
|
39
41
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistent-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/persistent-lifecycle.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,QAAQ,EAAE,MAAM,eAAe,CAAC;AAO5E,MAAM,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"persistent-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/persistent-lifecycle.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,QAAQ,EAAE,MAAM,eAAe,CAAC;AAO5E,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IACzC;;;OAGG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;GAGG;AACH,qBAAa,mBAAmB,CAAC,CAAC,CAAE,SAAQ,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,YAAY,CAAC,CAA2B;IAChD,OAAO,CAAC,aAAa,CAAK;gBAEd,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,eAA2C,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;cASxF,KAAK;cAgBL,MAAM;YAMjB,QAAQ;YAkBR,mBAAmB;IAWjC;;OAEG;IAEH,eAAe;CAMhB"}
|
|
@@ -10,6 +10,7 @@ type TestEdgeWsServerParams = {
|
|
|
10
10
|
export declare const createTestEdgeWsServer: (port?: number, params?: TestEdgeWsServerParams) => Promise<{
|
|
11
11
|
server: WebSocket.Server;
|
|
12
12
|
messageSink: any[];
|
|
13
|
+
messageSourceLog: any[];
|
|
13
14
|
endpoint: string;
|
|
14
15
|
cleanup: () => void;
|
|
15
16
|
currentConnection: () => WebSocket | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../../src/testing/test-utils.ts"],"names":[],"mappings":"AAIA,OAAO,SAAS,MAAM,eAAe,CAAC;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGtC,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,4CAA4C,CAAC;AAK5G,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,KAAK,sBAAsB,GAAG;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,GAAG,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;CACpE,CAAC;AAEF,eAAO,MAAM,sBAAsB,2BAAwC,sBAAsB
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../../src/testing/test-utils.ts"],"names":[],"mappings":"AAIA,OAAO,SAAS,MAAM,eAAe,CAAC;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGtC,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,4CAA4C,CAAC;AAK5G,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,KAAK,sBAAsB,GAAG;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,GAAG,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;CACpE,CAAC;AAEF,eAAO,MAAM,sBAAsB,2BAAwC,sBAAsB;;;;;;;mCAsE9E,OAAO,mBAAmB,UAAU;uBA1BhC,OAAO;;EAS7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/edge-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "EDGE Client",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -35,21 +35,21 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"isomorphic-ws": "^5.0.0",
|
|
37
37
|
"ws": "^8.14.2",
|
|
38
|
-
"@dxos/async": "0.
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/keys": "0.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
38
|
+
"@dxos/async": "0.7.0",
|
|
39
|
+
"@dxos/context": "0.7.0",
|
|
40
|
+
"@dxos/credentials": "0.7.0",
|
|
41
|
+
"@dxos/debug": "0.7.0",
|
|
42
|
+
"@dxos/crypto": "0.7.0",
|
|
43
|
+
"@dxos/keyring": "0.7.0",
|
|
44
|
+
"@dxos/invariant": "0.7.0",
|
|
45
|
+
"@dxos/keys": "0.7.0",
|
|
46
|
+
"@dxos/node-std": "0.7.0",
|
|
47
|
+
"@dxos/protocols": "0.7.0",
|
|
48
|
+
"@dxos/util": "0.7.0",
|
|
49
|
+
"@dxos/log": "0.7.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@dxos/test-utils": "0.
|
|
52
|
+
"@dxos/test-utils": "0.7.0"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
package/src/auth.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { createCredential, signPresentation } from '@dxos/credentials';
|
|
6
6
|
import { type Signer } from '@dxos/crypto';
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
7
8
|
import { Keyring } from '@dxos/keyring';
|
|
8
9
|
import { PublicKey } from '@dxos/keys';
|
|
9
10
|
import { type Chain, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
@@ -47,7 +48,7 @@ export const createChainEdgeIdentity = async (
|
|
|
47
48
|
signer: Signer,
|
|
48
49
|
identityKey: PublicKey,
|
|
49
50
|
peerKey: PublicKey,
|
|
50
|
-
chain: Chain,
|
|
51
|
+
chain: Chain | undefined,
|
|
51
52
|
credentials: Credential[],
|
|
52
53
|
): Promise<EdgeIdentity> => {
|
|
53
54
|
const credentialsToSign =
|
|
@@ -70,6 +71,8 @@ export const createChainEdgeIdentity = async (
|
|
|
70
71
|
identityKey: identityKey.toHex(),
|
|
71
72
|
peerKey: peerKey.toHex(),
|
|
72
73
|
presentCredentials: async ({ challenge }) => {
|
|
74
|
+
// TODO: make chain required after device invitation flow update release
|
|
75
|
+
invariant(chain);
|
|
73
76
|
return signPresentation({
|
|
74
77
|
presentation: {
|
|
75
78
|
credentials: credentialsToSign,
|
package/src/edge-client.test.ts
CHANGED
|
@@ -12,33 +12,33 @@ import { openAndClose } from '@dxos/test-utils';
|
|
|
12
12
|
import { createEphemeralEdgeIdentity, createTestHaloEdgeIdentity } from './auth';
|
|
13
13
|
import { protocol } from './defs';
|
|
14
14
|
import { EdgeClient } from './edge-client';
|
|
15
|
+
import { type EdgeIdentity } from './edge-identity';
|
|
16
|
+
import { EdgeConnectionClosedError, EdgeIdentityChangedError } from './errors';
|
|
15
17
|
import { createTestEdgeWsServer } from './testing';
|
|
16
18
|
|
|
17
19
|
describe('EdgeClient', () => {
|
|
18
|
-
|
|
20
|
+
let wsServerPort = 8001;
|
|
19
21
|
|
|
20
22
|
test('reconnects on error', async () => {
|
|
21
|
-
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(
|
|
23
|
+
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++);
|
|
22
24
|
onTestFinished(cleanup);
|
|
23
25
|
|
|
24
|
-
const client =
|
|
25
|
-
await openAndClose(client);
|
|
26
|
+
const { client, reconnectTrigger } = await openNewClient(endpoint);
|
|
26
27
|
await client.send(textMessage('Hello world 1'));
|
|
27
28
|
expect(client.isOpen).is.true;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
reconnectTrigger.reset();
|
|
30
31
|
await closeConnection();
|
|
31
|
-
await
|
|
32
|
+
await reconnectTrigger.wait();
|
|
32
33
|
await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
test('isConnected', async () => {
|
|
36
37
|
const admitConnection = new Trigger();
|
|
37
|
-
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(
|
|
38
|
+
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
|
|
38
39
|
onTestFinished(cleanup);
|
|
39
40
|
|
|
40
|
-
const client =
|
|
41
|
-
await openAndClose(client);
|
|
41
|
+
const { client } = await openNewClient(endpoint);
|
|
42
42
|
|
|
43
43
|
expect(client.isConnected).toBeFalsy();
|
|
44
44
|
admitConnection.wake();
|
|
@@ -54,20 +54,90 @@ describe('EdgeClient', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('set identity reconnects', async () => {
|
|
57
|
-
const { endpoint, cleanup } = await createTestEdgeWsServer(
|
|
57
|
+
const { endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++);
|
|
58
58
|
onTestFinished(cleanup);
|
|
59
59
|
|
|
60
|
-
const client =
|
|
61
|
-
await openAndClose(client);
|
|
60
|
+
const { client, reconnectTrigger } = await openNewClient(endpoint);
|
|
62
61
|
await client.send(textMessage('Hello world 1'));
|
|
63
62
|
expect(client.isOpen).is.true;
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
reconnectTrigger.reset();
|
|
66
65
|
client.setIdentity(await createEphemeralEdgeIdentity());
|
|
67
|
-
await
|
|
66
|
+
await reconnectTrigger.wait();
|
|
68
67
|
await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
|
|
69
68
|
});
|
|
70
69
|
|
|
70
|
+
test('send blocks until connection becomes ready', async () => {
|
|
71
|
+
const admitConnection = new Trigger();
|
|
72
|
+
const { endpoint, messageSink, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
|
|
73
|
+
onTestFinished(cleanup);
|
|
74
|
+
|
|
75
|
+
const { client } = await openNewClient(endpoint);
|
|
76
|
+
setTimeout(() => admitConnection.wake(), 20);
|
|
77
|
+
await client.send(textMessage('Hello world 1'));
|
|
78
|
+
await expect.poll(() => messageSink.length).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('send fails if identity changes before connection becomes ready', async () => {
|
|
82
|
+
const admitConnection = new Trigger();
|
|
83
|
+
const { endpoint, cleanup, messageSink } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
|
|
84
|
+
onTestFinished(cleanup);
|
|
85
|
+
|
|
86
|
+
const { client } = await openNewClient(endpoint);
|
|
87
|
+
setTimeout(async () => client.setIdentity(await createEphemeralEdgeIdentity()));
|
|
88
|
+
await expect(client.send(textMessage('Hello world 1'))).rejects.toThrow(EdgeIdentityChangedError);
|
|
89
|
+
|
|
90
|
+
// Test recovers.
|
|
91
|
+
setTimeout(() => admitConnection.wake(), 20);
|
|
92
|
+
await client.send(textMessage('Hello world 1'));
|
|
93
|
+
await expect.poll(() => messageSink.length).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('send fails if client is closed before connection becomes ready', async () => {
|
|
97
|
+
const admitConnection = new Trigger();
|
|
98
|
+
const { endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
|
|
99
|
+
onTestFinished(cleanup);
|
|
100
|
+
|
|
101
|
+
const { client } = await openNewClient(endpoint);
|
|
102
|
+
setTimeout(() => client.close());
|
|
103
|
+
await expect(client.send(textMessage('Hello world 1'))).rejects.toThrow(EdgeConnectionClosedError);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('onReconnect trigger', async () => {
|
|
107
|
+
const admitConnection = new Trigger();
|
|
108
|
+
const { endpoint, cleanup, closeConnection } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
|
|
109
|
+
onTestFinished(cleanup);
|
|
110
|
+
|
|
111
|
+
const { client } = await openNewClient(endpoint);
|
|
112
|
+
let callCount = 0;
|
|
113
|
+
client.onReconnected(() => callCount++);
|
|
114
|
+
admitConnection.wake();
|
|
115
|
+
|
|
116
|
+
await expect.poll(() => callCount).toEqual(1);
|
|
117
|
+
await closeConnection();
|
|
118
|
+
await expect.poll(() => callCount).toEqual(2);
|
|
119
|
+
|
|
120
|
+
const trigger = new Trigger();
|
|
121
|
+
client.onReconnected(() => trigger.wake());
|
|
122
|
+
await trigger.wait();
|
|
123
|
+
expect(callCount).toEqual(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('send message right after identity change is delivered successfully with the new identity', async () => {
|
|
127
|
+
const { endpoint, cleanup, messageSourceLog } = await createTestEdgeWsServer(wsServerPort++);
|
|
128
|
+
onTestFinished(cleanup);
|
|
129
|
+
|
|
130
|
+
const { client, identity: oldIdentity } = await openNewClient(endpoint);
|
|
131
|
+
await client.send(textMessage('Hello world 1', oldIdentity));
|
|
132
|
+
expect(client.isOpen).is.true;
|
|
133
|
+
|
|
134
|
+
const newIdentity = await createEphemeralEdgeIdentity();
|
|
135
|
+
client.setIdentity(newIdentity);
|
|
136
|
+
await client.send(textMessage('Hello world 2', newIdentity));
|
|
137
|
+
await expect.poll(() => messageSourceLog.length).toBe(2);
|
|
138
|
+
expect(messageSourceLog.map((m) => m.peerKey)).toStrictEqual([oldIdentity.peerKey, newIdentity.peerKey]);
|
|
139
|
+
});
|
|
140
|
+
|
|
71
141
|
test.skipIf(!process.env.EDGE_ENDPOINT)('connect to local edge server', async () => {
|
|
72
142
|
// const identity = await createEphemeralEdgeIdentity();
|
|
73
143
|
|
|
@@ -79,4 +149,21 @@ describe('EdgeClient', () => {
|
|
|
79
149
|
await client.send(textMessage('Hello world 1'));
|
|
80
150
|
expect(client.isOpen).is.true;
|
|
81
151
|
});
|
|
152
|
+
|
|
153
|
+
const textMessage = (message: string, source?: EdgeIdentity) =>
|
|
154
|
+
protocol.createMessage(TextMessageSchema, {
|
|
155
|
+
source: source && { peerKey: source.peerKey, identityKey: source.identityKey },
|
|
156
|
+
payload: { message },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const openNewClient = async (endpoint: string) => {
|
|
160
|
+
const identity = await createEphemeralEdgeIdentity();
|
|
161
|
+
const client = new EdgeClient(identity, { socketEndpoint: endpoint });
|
|
162
|
+
await openAndClose(client);
|
|
163
|
+
const reconnectTrigger = new Trigger();
|
|
164
|
+
client.onReconnected(() => {
|
|
165
|
+
reconnectTrigger.wake();
|
|
166
|
+
});
|
|
167
|
+
return { client, reconnectTrigger, identity };
|
|
168
|
+
};
|
|
82
169
|
});
|
package/src/edge-client.ts
CHANGED
|
@@ -2,37 +2,33 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { log } from '@dxos/log';
|
|
10
|
-
import { buf } from '@dxos/protocols/buf';
|
|
11
|
-
import { type Message, MessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
5
|
+
import { Trigger, scheduleMicroTask, TriggerState } from '@dxos/async';
|
|
6
|
+
import { Resource, type Lifecycle } from '@dxos/context';
|
|
7
|
+
import { log, logInfo } from '@dxos/log';
|
|
8
|
+
import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
12
9
|
|
|
13
10
|
import { protocol } from './defs';
|
|
14
11
|
import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
|
|
12
|
+
import { EdgeWsConnection } from './edge-ws-connection';
|
|
15
13
|
import { EdgeConnectionClosedError, EdgeIdentityChangedError } from './errors';
|
|
16
14
|
import { PersistentLifecycle } from './persistent-lifecycle';
|
|
17
|
-
import { type Protocol
|
|
15
|
+
import { type Protocol } from './protocol';
|
|
18
16
|
import { getEdgeUrlWithProtocol } from './utils';
|
|
19
17
|
|
|
20
18
|
const DEFAULT_TIMEOUT = 10_000;
|
|
21
|
-
const SIGNAL_KEEPALIVE_INTERVAL = 5_000;
|
|
22
19
|
|
|
23
|
-
export type MessageListener = (message: Message) => void
|
|
20
|
+
export type MessageListener = (message: Message) => void;
|
|
21
|
+
export type ReconnectListener = () => void;
|
|
24
22
|
|
|
25
23
|
export interface EdgeConnection extends Required<Lifecycle> {
|
|
26
|
-
connected: Event;
|
|
27
|
-
reconnect: Event;
|
|
28
|
-
|
|
29
24
|
get info(): any;
|
|
30
25
|
get identityKey(): string;
|
|
31
26
|
get peerKey(): string;
|
|
32
27
|
get isOpen(): boolean;
|
|
33
28
|
get isConnected(): boolean;
|
|
34
29
|
setIdentity(identity: EdgeIdentity): void;
|
|
35
|
-
|
|
30
|
+
onMessage(listener: MessageListener): () => void;
|
|
31
|
+
onReconnected(listener: ReconnectListener): () => void;
|
|
36
32
|
send(message: Message): Promise<void>;
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -44,25 +40,25 @@ export type MessengerConfig = {
|
|
|
44
40
|
};
|
|
45
41
|
|
|
46
42
|
/**
|
|
47
|
-
* Messenger client
|
|
43
|
+
* Messenger client for EDGE:
|
|
44
|
+
* - While open, uses PersistentLifecycle to keep an open EdgeWsConnection, reconnecting on failures.
|
|
45
|
+
* - Manages identity and re-create EdgeWsConnection when identity changes.
|
|
46
|
+
* - Dispatches connection state and message notifications.
|
|
48
47
|
*/
|
|
49
48
|
export class EdgeClient extends Resource implements EdgeConnection {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
start: async () => this._openWebSocket(),
|
|
54
|
-
stop: async () => this._closeWebSocket(),
|
|
55
|
-
onRestart: async () => this.reconnect.emit(),
|
|
49
|
+
private readonly _persistentLifecycle = new PersistentLifecycle<EdgeWsConnection>({
|
|
50
|
+
start: async () => this._connect(),
|
|
51
|
+
stop: async (state: EdgeWsConnection) => this._disconnect(state),
|
|
56
52
|
});
|
|
57
53
|
|
|
58
|
-
private readonly
|
|
59
|
-
private
|
|
60
|
-
private _ws?: WebSocket = undefined;
|
|
61
|
-
private _keepaliveCtx?: Context = undefined;
|
|
62
|
-
private _heartBeatContext?: Context = undefined;
|
|
54
|
+
private readonly _messageListeners = new Set<MessageListener>();
|
|
55
|
+
private readonly _reconnectListeners = new Set<ReconnectListener>();
|
|
63
56
|
|
|
64
|
-
private _baseWsUrl: string;
|
|
65
|
-
private _baseHttpUrl: string;
|
|
57
|
+
private readonly _baseWsUrl: string;
|
|
58
|
+
private readonly _baseHttpUrl: string;
|
|
59
|
+
|
|
60
|
+
private _currentConnection?: EdgeWsConnection = undefined;
|
|
61
|
+
private _ready = new Trigger();
|
|
66
62
|
|
|
67
63
|
constructor(
|
|
68
64
|
private _identity: EdgeIdentity,
|
|
@@ -73,7 +69,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
73
69
|
this._baseHttpUrl = getEdgeUrlWithProtocol(_config.socketEndpoint, 'http');
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
@logInfo
|
|
77
73
|
public get info() {
|
|
78
74
|
return {
|
|
79
75
|
open: this.isOpen,
|
|
@@ -83,7 +79,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
get isConnected() {
|
|
86
|
-
return Boolean(this.
|
|
82
|
+
return Boolean(this._currentConnection) && this._ready.state === TriggerState.RESOLVED;
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
get identityKey() {
|
|
@@ -98,13 +94,32 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
98
94
|
if (identity.identityKey !== this._identity.identityKey || identity.peerKey !== this._identity.peerKey) {
|
|
99
95
|
log('Edge identity changed', { identity, oldIdentity: this._identity });
|
|
100
96
|
this._identity = identity;
|
|
97
|
+
this._closeCurrentConnection(new EdgeIdentityChangedError());
|
|
101
98
|
this._persistentLifecycle.scheduleRestart();
|
|
102
99
|
}
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
public
|
|
106
|
-
this.
|
|
107
|
-
return () => this.
|
|
102
|
+
public onMessage(listener: MessageListener): () => void {
|
|
103
|
+
this._messageListeners.add(listener);
|
|
104
|
+
return () => this._messageListeners.delete(listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public onReconnected(listener: () => void): () => void {
|
|
108
|
+
this._reconnectListeners.add(listener);
|
|
109
|
+
if (this._ready.state === TriggerState.RESOLVED) {
|
|
110
|
+
// Microtask so that listener is always called asynchronously, no matter the state of the ready trigger
|
|
111
|
+
// at the moment of registration.
|
|
112
|
+
scheduleMicroTask(this._ctx, () => {
|
|
113
|
+
if (this._reconnectListeners.has(listener)) {
|
|
114
|
+
try {
|
|
115
|
+
listener();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log.catch(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return () => this._reconnectListeners.delete(listener);
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
/**
|
|
@@ -122,97 +137,96 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
122
137
|
*/
|
|
123
138
|
protected override async _close() {
|
|
124
139
|
log('closing...', { peerKey: this._identity.peerKey });
|
|
140
|
+
this._closeCurrentConnection();
|
|
125
141
|
await this._persistentLifecycle.close();
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
private async
|
|
144
|
+
private async _connect(): Promise<EdgeWsConnection | undefined> {
|
|
129
145
|
if (this._ctx.disposed) {
|
|
130
|
-
return;
|
|
146
|
+
return undefined;
|
|
131
147
|
}
|
|
132
|
-
|
|
148
|
+
|
|
149
|
+
const identity = this._identity;
|
|
150
|
+
const path = `/ws/${identity.identityKey}/${identity.peerKey}`;
|
|
133
151
|
const protocolHeader = this._config.disableAuth ? undefined : await this._createAuthHeader(path);
|
|
152
|
+
if (this._identity !== identity) {
|
|
153
|
+
log('identity changed during auth header request');
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
134
156
|
|
|
157
|
+
const restartRequired = new Trigger();
|
|
135
158
|
const url = new URL(path, this._baseWsUrl);
|
|
136
159
|
log('Opening websocket', { url: url.toString(), protocolHeader });
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
148
|
-
this._ws.onerror = (event) => {
|
|
149
|
-
log.warn('EdgeClient socket error', { error: event.error, info: event.message });
|
|
150
|
-
this._persistentLifecycle.scheduleRestart();
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
|
154
|
-
*/
|
|
155
|
-
this._ws.onmessage = async (event) => {
|
|
156
|
-
if (event.data === '__pong__') {
|
|
157
|
-
this._onHeartbeat();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const data = await toUint8Array(event.data);
|
|
161
|
-
const message = buf.fromBinary(MessageSchema, data);
|
|
162
|
-
log('received', { peerKey: this._identity.peerKey, payload: protocol.getPayloadType(message) });
|
|
163
|
-
if (message) {
|
|
164
|
-
for (const listener of this._listeners) {
|
|
165
|
-
try {
|
|
166
|
-
await listener(message);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
log.error('processing', { err, payload: protocol.getPayloadType(message) });
|
|
160
|
+
const connection = new EdgeWsConnection(
|
|
161
|
+
identity,
|
|
162
|
+
{ url, protocolHeader },
|
|
163
|
+
{
|
|
164
|
+
onConnected: () => {
|
|
165
|
+
if (this._isActive(connection)) {
|
|
166
|
+
this._ready.wake();
|
|
167
|
+
this._notifyReconnected();
|
|
168
|
+
} else {
|
|
169
|
+
log.verbose('connected callback ignored, because connection is not active');
|
|
169
170
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
},
|
|
172
|
+
onRestartRequired: () => {
|
|
173
|
+
if (this._isActive(connection)) {
|
|
174
|
+
this._closeCurrentConnection();
|
|
175
|
+
this._persistentLifecycle.scheduleRestart();
|
|
176
|
+
} else {
|
|
177
|
+
log.verbose('restart requested by inactive connection');
|
|
178
|
+
}
|
|
179
|
+
restartRequired.wake();
|
|
180
|
+
},
|
|
181
|
+
onMessage: (message) => {
|
|
182
|
+
if (this._isActive(connection)) {
|
|
183
|
+
this._notifyMessageReceived(message);
|
|
184
|
+
} else {
|
|
185
|
+
log.verbose('ignored a message on inactive connection', {
|
|
186
|
+
from: message.source,
|
|
187
|
+
type: message.payload?.typeUrl,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
},
|
|
186
191
|
},
|
|
187
|
-
SIGNAL_KEEPALIVE_INTERVAL,
|
|
188
192
|
);
|
|
189
|
-
this.
|
|
190
|
-
|
|
193
|
+
this._currentConnection = connection;
|
|
194
|
+
|
|
195
|
+
await connection.open();
|
|
196
|
+
// Race with restartRequired so that restart is not blocked by _connect execution.
|
|
197
|
+
// Wait on ready to attempt a reconnect if it times out.
|
|
198
|
+
await Promise.race([this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT }), restartRequired]);
|
|
199
|
+
|
|
200
|
+
return connection;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async _disconnect(state: EdgeWsConnection) {
|
|
204
|
+
await state.close();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private _closeCurrentConnection(error: Error = new EdgeConnectionClosedError()) {
|
|
208
|
+
this._currentConnection = undefined;
|
|
209
|
+
this._ready.throw(error);
|
|
210
|
+
this._ready.reset();
|
|
191
211
|
}
|
|
192
212
|
|
|
193
|
-
private
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
private _notifyReconnected() {
|
|
214
|
+
for (const listener of this._reconnectListeners) {
|
|
215
|
+
try {
|
|
216
|
+
listener();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
log.error('ws reconnect listener failed', { err });
|
|
219
|
+
}
|
|
196
220
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// NOTE: Remove event handlers to avoid scheduling restart.
|
|
206
|
-
this._ws.onopen = () => {};
|
|
207
|
-
this._ws.onclose = () => {};
|
|
208
|
-
this._ws.onerror = () => {};
|
|
209
|
-
this._ws.close();
|
|
210
|
-
this._ws = undefined;
|
|
211
|
-
} catch (err) {
|
|
212
|
-
if (err instanceof Error && err.message.includes('WebSocket is closed before the connection is established.')) {
|
|
213
|
-
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private _notifyMessageReceived(message: Message) {
|
|
224
|
+
for (const listener of this._messageListeners) {
|
|
225
|
+
try {
|
|
226
|
+
listener(message);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
log.error('ws incoming message processing failed', { err, payload: protocol.getPayloadType(message) });
|
|
214
229
|
}
|
|
215
|
-
log.warn('Error closing websocket', { err });
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
|
|
@@ -225,9 +239,11 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
225
239
|
log('waiting for websocket to become ready');
|
|
226
240
|
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
227
241
|
}
|
|
228
|
-
|
|
242
|
+
|
|
243
|
+
if (!this._currentConnection) {
|
|
229
244
|
throw new EdgeConnectionClosedError();
|
|
230
245
|
}
|
|
246
|
+
|
|
231
247
|
if (
|
|
232
248
|
message.source &&
|
|
233
249
|
(message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
|
|
@@ -235,23 +251,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
235
251
|
throw new EdgeIdentityChangedError();
|
|
236
252
|
}
|
|
237
253
|
|
|
238
|
-
|
|
239
|
-
this._ws.send(buf.toBinary(MessageSchema, message));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private _onHeartbeat() {
|
|
243
|
-
if (this._lifecycleState !== LifecycleState.OPEN) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
void this._heartBeatContext?.dispose();
|
|
247
|
-
this._heartBeatContext = new Context();
|
|
248
|
-
scheduleTask(
|
|
249
|
-
this._heartBeatContext,
|
|
250
|
-
() => {
|
|
251
|
-
this._persistentLifecycle.scheduleRestart();
|
|
252
|
-
},
|
|
253
|
-
2 * SIGNAL_KEEPALIVE_INTERVAL,
|
|
254
|
-
);
|
|
254
|
+
this._currentConnection.send(message);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
private async _createAuthHeader(path: string): Promise<string | undefined> {
|
|
@@ -265,6 +265,8 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
265
265
|
return undefined;
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
private _isActive = (connection: EdgeWsConnection) => connection === this._currentConnection;
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
const encodePresentationWsAuthHeader = (encodedPresentation: Uint8Array): string => {
|