@dxos/edge-client 0.6.9 → 0.6.10-main.48c066e

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 (34) hide show
  1. package/dist/lib/browser/index.mjs +276 -138
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +263 -129
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/{client.d.ts → edge-client.d.ts} +21 -16
  8. package/dist/types/src/edge-client.d.ts.map +1 -0
  9. package/dist/types/src/edge-client.test.d.ts +2 -0
  10. package/dist/types/src/edge-client.test.d.ts.map +1 -0
  11. package/dist/types/src/errors.d.ts +4 -0
  12. package/dist/types/src/errors.d.ts.map +1 -0
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/index.d.ts.map +1 -1
  15. package/dist/types/src/persistent-lifecycle.d.ts +42 -0
  16. package/dist/types/src/persistent-lifecycle.d.ts.map +1 -0
  17. package/dist/types/src/persistent-lifecycle.test.d.ts +2 -0
  18. package/dist/types/src/persistent-lifecycle.test.d.ts.map +1 -0
  19. package/dist/types/src/test-utils.d.ts +11 -0
  20. package/dist/types/src/test-utils.d.ts.map +1 -0
  21. package/dist/types/src/websocket.test.d.ts +2 -0
  22. package/dist/types/src/websocket.test.d.ts.map +1 -0
  23. package/package.json +13 -9
  24. package/src/edge-client.test.ts +50 -0
  25. package/src/edge-client.ts +226 -0
  26. package/src/errors.ts +9 -0
  27. package/src/index.ts +1 -1
  28. package/src/persistent-lifecycle.test.ts +71 -0
  29. package/src/persistent-lifecycle.ts +106 -0
  30. package/src/protocol.test.ts +1 -1
  31. package/src/test-utils.ts +49 -0
  32. package/src/websocket.test.ts +35 -0
  33. package/dist/types/src/client.d.ts.map +0 -1
  34. package/src/client.ts +0 -179
@@ -1 +1 @@
1
- {"inputs":{"packages/core/mesh/edge-client/src/protocol.ts":{"bytes":10314,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"format":"esm"},"packages/core/mesh/edge-client/src/defs.ts":{"bytes":1578,"imports":[{"path":"@bufbuild/protobuf/wkt","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"},"packages/core/mesh/edge-client/src/client.ts":{"bytes":20675,"imports":[{"path":"isomorphic-ws","kind":"import-statement","external":true},{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/defs.ts","kind":"import-statement","original":"./defs"},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"},"packages/core/mesh/edge-client/src/index.ts":{"bytes":836,"imports":[{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/client.ts","kind":"import-statement","original":"./client"},{"path":"packages/core/mesh/edge-client/src/defs.ts","kind":"import-statement","original":"./defs"},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"}},"outputs":{"packages/core/mesh/edge-client/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":14946},"packages/core/mesh/edge-client/dist/lib/node/index.cjs":{"imports":[{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"isomorphic-ws","kind":"import-statement","external":true},{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@bufbuild/protobuf/wkt","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["EdgeClient","Protocol","getTypename","protocol","toUint8Array"],"entryPoint":"packages/core/mesh/edge-client/src/index.ts","inputs":{"packages/core/mesh/edge-client/src/index.ts":{"bytesInOutput":60},"packages/core/mesh/edge-client/src/client.ts":{"bytesInOutput":5789},"packages/core/mesh/edge-client/src/defs.ts":{"bytesInOutput":285},"packages/core/mesh/edge-client/src/protocol.ts":{"bytesInOutput":2600}},"bytes":9139}}}
1
+ {"inputs":{"packages/core/mesh/edge-client/src/protocol.ts":{"bytes":10314,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"format":"esm"},"packages/core/mesh/edge-client/src/defs.ts":{"bytes":1578,"imports":[{"path":"@bufbuild/protobuf/wkt","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"},"packages/core/mesh/edge-client/src/errors.ts":{"bytes":846,"imports":[],"format":"esm"},"packages/core/mesh/edge-client/src/persistent-lifecycle.ts":{"bytes":10868,"imports":[{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/debug","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true}],"format":"esm"},"packages/core/mesh/edge-client/src/edge-client.ts":{"bytes":26825,"imports":[{"path":"isomorphic-ws","kind":"import-statement","external":true},{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/defs.ts","kind":"import-statement","original":"./defs"},{"path":"packages/core/mesh/edge-client/src/errors.ts","kind":"import-statement","original":"./errors"},{"path":"packages/core/mesh/edge-client/src/persistent-lifecycle.ts","kind":"import-statement","original":"./persistent-lifecycle"},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"},"packages/core/mesh/edge-client/src/index.ts":{"bytes":849,"imports":[{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"packages/core/mesh/edge-client/src/edge-client.ts","kind":"import-statement","original":"./edge-client"},{"path":"packages/core/mesh/edge-client/src/defs.ts","kind":"import-statement","original":"./defs"},{"path":"packages/core/mesh/edge-client/src/protocol.ts","kind":"import-statement","original":"./protocol"}],"format":"esm"}},"outputs":{"packages/core/mesh/edge-client/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":23321},"packages/core/mesh/edge-client/dist/lib/node/index.cjs":{"imports":[{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"isomorphic-ws","kind":"import-statement","external":true},{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@bufbuild/protobuf/wkt","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf","kind":"import-statement","external":true},{"path":"@dxos/protocols/buf/dxos/edge/messenger_pb","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true},{"path":"@dxos/async","kind":"import-statement","external":true},{"path":"@dxos/context","kind":"import-statement","external":true},{"path":"@dxos/debug","kind":"import-statement","external":true},{"path":"@dxos/log","kind":"import-statement","external":true}],"exports":["EdgeClient","Protocol","getTypename","protocol","toUint8Array"],"entryPoint":"packages/core/mesh/edge-client/src/index.ts","inputs":{"packages/core/mesh/edge-client/src/index.ts":{"bytesInOutput":60},"packages/core/mesh/edge-client/src/edge-client.ts":{"bytesInOutput":7041},"packages/core/mesh/edge-client/src/defs.ts":{"bytesInOutput":285},"packages/core/mesh/edge-client/src/protocol.ts":{"bytesInOutput":2600},"packages/core/mesh/edge-client/src/errors.ts":{"bytesInOutput":116},"packages/core/mesh/edge-client/src/persistent-lifecycle.ts":{"bytesInOutput":3041}},"bytes":13670}}}
@@ -1,16 +1,17 @@
1
+ import { Event } from '@dxos/async';
1
2
  import { Resource, type Lifecycle } from '@dxos/context';
2
- import { type PublicKey } from '@dxos/keys';
3
3
  import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
4
4
  import { type Protocol } from './protocol';
5
5
  export type MessageListener = (message: Message) => void | Promise<void>;
6
6
  export interface EdgeConnection extends Required<Lifecycle> {
7
+ reconnect: Event;
7
8
  get info(): any;
8
- get identityKey(): PublicKey;
9
- get deviceKey(): PublicKey;
9
+ get identityKey(): string;
10
+ get peerKey(): string;
10
11
  get isOpen(): boolean;
11
12
  setIdentity(params: {
12
- deviceKey: PublicKey;
13
- identityKey: PublicKey;
13
+ peerKey: string;
14
+ identityKey: string;
14
15
  }): void;
15
16
  addListener(listener: MessageListener): () => void;
16
17
  send(message: Message): Promise<void>;
@@ -25,25 +26,28 @@ export type MessengerConfig = {
25
26
  */
26
27
  export declare class EdgeClient extends Resource implements EdgeConnection {
27
28
  private _identityKey;
28
- private _deviceKey;
29
+ private _peerKey;
29
30
  private readonly _config;
31
+ reconnect: Event<void>;
32
+ private readonly _persistentLifecycle;
30
33
  private readonly _listeners;
31
- private _reconnect?;
32
34
  private readonly _protocol;
33
35
  private _ready;
34
36
  private _ws?;
35
- constructor(_identityKey: PublicKey, _deviceKey: PublicKey, _config: MessengerConfig);
37
+ private _keepaliveCtx?;
38
+ private _heartBeatContext?;
39
+ constructor(_identityKey: string, _peerKey: string, _config: MessengerConfig);
36
40
  get info(): {
37
41
  open: boolean;
38
- identity: PublicKey;
39
- device: PublicKey;
42
+ identity: string;
43
+ device: string;
40
44
  };
41
- get identityKey(): PublicKey;
42
- get deviceKey(): PublicKey;
45
+ get identityKey(): string;
46
+ get peerKey(): string;
43
47
  get isOpen(): boolean;
44
- setIdentity({ deviceKey, identityKey }: {
45
- deviceKey: PublicKey;
46
- identityKey: PublicKey;
48
+ setIdentity({ peerKey, identityKey }: {
49
+ peerKey: string;
50
+ identityKey: string;
47
51
  }): void;
48
52
  addListener(listener: MessageListener): () => void;
49
53
  /**
@@ -61,5 +65,6 @@ export declare class EdgeClient extends Resource implements EdgeConnection {
61
65
  * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
62
66
  */
63
67
  send(message: Message): Promise<void>;
68
+ private _onHeartbeat;
64
69
  }
65
- //# sourceMappingURL=client.d.ts.map
70
+ //# sourceMappingURL=edge-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-client.d.ts","sourceRoot":"","sources":["../../../src/edge-client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAW,KAAK,EAAoD,MAAM,aAAa,CAAC;AAC/F,OAAO,EAA2B,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAIlF,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,4CAA4C,CAAC;AAKzF,OAAO,EAAE,KAAK,QAAQ,EAAgB,MAAM,YAAY,CAAC;AAKzD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEzE,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAAC,SAAS,CAAC;IACzD,SAAS,EAAE,KAAK,CAAC;IAEjB,IAAI,IAAI,IAAI,GAAG,CAAC;IAChB,IAAI,WAAW,IAAI,MAAM,CAAC;IAC1B,IAAI,OAAO,IAAI,MAAM,CAAC;IACtB,IAAI,MAAM,IAAI,OAAO,CAAC;IACtB,WAAW,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpE,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,IAAI,CAAC;IACnD,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB,CAAC;AAEF;;GAEG;AACH,qBAAa,UAAW,SAAQ,QAAS,YAAW,cAAc;IAgB9D,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAjBnB,SAAS,cAAe;IAC/B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAIlC;IAEH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,GAAG,CAAC,CAAwB;IACpC,OAAO,CAAC,aAAa,CAAC,CAAsB;IAC5C,OAAO,CAAC,iBAAiB,CAAC,CAAsB;gBAGtC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EACP,OAAO,EAAE,eAAe;IAO3C,IAAW,IAAI;;;;MAMd;IAED,IAAI,WAAW,WAEd;IAED,IAAI,OAAO,WAEV;IAED,IAAW,MAAM,YAEhB;IAED,WAAW,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAMvE,WAAW,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,IAAI;IAKzD;;OAEG;cACsB,KAAK;IAO9B;;OAEG;cACsB,MAAM;YAKjB,cAAc;YAqDd,eAAe;IA0B7B;;;OAGG;IACU,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlD,OAAO,CAAC,YAAY;CAcrB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=edge-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-client.test.d.ts","sourceRoot":"","sources":["../../../src/edge-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export declare class WebsocketClosedError extends Error {
2
+ constructor();
3
+ }
4
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/errors.ts"],"names":[],"mappings":"AAIA,qBAAa,oBAAqB,SAAQ,KAAK;;CAI9C"}
@@ -1,5 +1,5 @@
1
1
  export * from '@dxos/protocols/buf/dxos/edge/messenger_pb';
2
- export * from './client';
2
+ export * from './edge-client';
3
3
  export * from './defs';
4
4
  export * from './protocol';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,4CAA4C,CAAC;AAE3D,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,4CAA4C,CAAC;AAE3D,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { Resource } from '@dxos/context';
2
+ export type PersistentLifecycleParams = {
3
+ /**
4
+ * Create connection.
5
+ * If promise resolves successfully, connection is considered established.
6
+ */
7
+ start: () => Promise<void>;
8
+ /**
9
+ * Reset connection to initial state.
10
+ */
11
+ stop: () => Promise<void>;
12
+ /**
13
+ * Called after successful start.
14
+ */
15
+ onRestart?: () => Promise<void>;
16
+ /**
17
+ * Maximum delay between restartion attempts.
18
+ * Default: 5000ms
19
+ */
20
+ maxRestartDelay?: number;
21
+ };
22
+ /**
23
+ * Handles restarts (e.g. persists connection).
24
+ * Restarts are scheduled with exponential backoff.
25
+ */
26
+ export declare class PersistentLifecycle extends Resource {
27
+ private readonly _start;
28
+ private readonly _stop;
29
+ private readonly _onRestart?;
30
+ private readonly _maxRestartDelay;
31
+ private _restartTask?;
32
+ private _restartAfter;
33
+ constructor({ start, stop, onRestart, maxRestartDelay }: PersistentLifecycleParams);
34
+ protected _open(): Promise<void>;
35
+ protected _close(): Promise<void>;
36
+ private _restart;
37
+ /**
38
+ * Scheduling restart should be done from outside.
39
+ */
40
+ scheduleRestart(): void;
41
+ }
42
+ //# sourceMappingURL=persistent-lifecycle.d.ts.map
@@ -0,0 +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,GAAG;IACtC;;;OAGG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;OAEG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;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,mBAAoB,SAAQ,QAAQ;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,OAAO,CAAC,YAAY,CAAC,CAA2B;IAChD,OAAO,CAAC,aAAa,CAAK;gBAEd,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,eAA2C,EAAE,EAAE,yBAAyB;cASrF,KAAK;cAeL,MAAM;YAMjB,QAAQ;IAgBtB;;OAEG;IAEH,eAAe;CAMhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=persistent-lifecycle.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistent-lifecycle.test.d.ts","sourceRoot":"","sources":["../../../src/persistent-lifecycle.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=websocket.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.test.d.ts","sourceRoot":"","sources":["../../../src/websocket.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/edge-client",
3
- "version": "0.6.9",
3
+ "version": "0.6.10-main.48c066e",
4
4
  "description": "EDGE Client",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -28,15 +28,19 @@
28
28
  "@bufbuild/protobuf": "^2.0.0",
29
29
  "isomorphic-ws": "^5.0.0",
30
30
  "ws": "^8.14.2",
31
- "@dxos/async": "0.6.9",
32
- "@dxos/context": "0.6.9",
33
- "@dxos/invariant": "0.6.9",
34
- "@dxos/node-std": "0.6.9",
35
- "@dxos/log": "0.6.9",
36
- "@dxos/protocols": "0.6.9",
37
- "@dxos/util": "0.6.9"
31
+ "@dxos/async": "0.6.10-main.48c066e",
32
+ "@dxos/context": "0.6.10-main.48c066e",
33
+ "@dxos/debug": "0.6.10-main.48c066e",
34
+ "@dxos/invariant": "0.6.10-main.48c066e",
35
+ "@dxos/log": "0.6.10-main.48c066e",
36
+ "@dxos/protocols": "0.6.10-main.48c066e",
37
+ "@dxos/util": "0.6.10-main.48c066e",
38
+ "@dxos/node-std": "0.6.10-main.48c066e"
39
+ },
40
+ "devDependencies": {
41
+ "jest-websocket-mock": "^2.5.0",
42
+ "@dxos/keys": "0.6.10-main.48c066e"
38
43
  },
39
- "devDependencies": {},
40
44
  "publishConfig": {
41
45
  "access": "public"
42
46
  }
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import chai, { expect } from 'chai';
6
+ import chaiAsPromised from 'chai-as-promised';
7
+
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { TextMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
10
+ import { test, describe, openAndClose } from '@dxos/test';
11
+
12
+ import { protocol } from './defs';
13
+ import { EdgeClient } from './edge-client';
14
+ import { createTestWsServer } from './test-utils';
15
+
16
+ chai.use(chaiAsPromised);
17
+
18
+ describe('EdgeClient', () => {
19
+ const textMessage = (message: string) => protocol.createMessage(TextMessageSchema, { payload: { message } });
20
+
21
+ test('reconnects on error', async () => {
22
+ const { error: serverError, endpoint } = await createTestWsServer();
23
+ const id = PublicKey.random().toHex();
24
+ const client = new EdgeClient(id, id, { socketEndpoint: endpoint });
25
+ await openAndClose(client);
26
+ await client.send(textMessage('Hello world 1'));
27
+ expect(client.isOpen).is.true;
28
+
29
+ const reconnected = client.reconnect.waitForCount(1);
30
+ await serverError();
31
+ await reconnected;
32
+ await expect(client.send(textMessage('Hello world 2'))).to.be.fulfilled;
33
+ });
34
+
35
+ test('set identity reconnects', async () => {
36
+ const { endpoint } = await createTestWsServer();
37
+
38
+ const id = PublicKey.random().toHex();
39
+ const client = new EdgeClient(id, id, { socketEndpoint: endpoint });
40
+ await openAndClose(client);
41
+ await client.send(textMessage('Hello world 1'));
42
+ expect(client.isOpen).is.true;
43
+
44
+ const newId = PublicKey.random().toHex();
45
+ const reconnected = client.reconnect.waitForCount(1);
46
+ client.setIdentity({ peerKey: newId, identityKey: newId });
47
+ await reconnected;
48
+ await expect(client.send(textMessage('Hello world 2'))).to.be.fulfilled;
49
+ });
50
+ });
@@ -0,0 +1,226 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import WebSocket from 'isomorphic-ws';
6
+
7
+ import { Trigger, Event, scheduleTaskInterval, scheduleTask, TriggerState } from '@dxos/async';
8
+ import { Context, LifecycleState, Resource, type Lifecycle } from '@dxos/context';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { log } from '@dxos/log';
11
+ import { buf } from '@dxos/protocols/buf';
12
+ import { type Message, MessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
13
+
14
+ import { protocol } from './defs';
15
+ import { WebsocketClosedError } from './errors';
16
+ import { PersistentLifecycle } from './persistent-lifecycle';
17
+ import { type Protocol, toUint8Array } from './protocol';
18
+
19
+ const DEFAULT_TIMEOUT = 10_000;
20
+ const SIGNAL_KEEPALIVE_INTERVAL = 5_000;
21
+
22
+ export type MessageListener = (message: Message) => void | Promise<void>;
23
+
24
+ export interface EdgeConnection extends Required<Lifecycle> {
25
+ reconnect: Event;
26
+
27
+ get info(): any;
28
+ get identityKey(): string;
29
+ get peerKey(): string;
30
+ get isOpen(): boolean;
31
+ setIdentity(params: { peerKey: string; identityKey: string }): void;
32
+ addListener(listener: MessageListener): () => void;
33
+ send(message: Message): Promise<void>;
34
+ }
35
+
36
+ export type MessengerConfig = {
37
+ socketEndpoint: string;
38
+ timeout?: number;
39
+ protocol?: Protocol;
40
+ };
41
+
42
+ /**
43
+ * Messenger client.
44
+ */
45
+ export class EdgeClient extends Resource implements EdgeConnection {
46
+ public reconnect = new Event();
47
+ private readonly _persistentLifecycle = new PersistentLifecycle({
48
+ start: async () => this._openWebSocket(),
49
+ stop: async () => this._closeWebSocket(),
50
+ onRestart: async () => this.reconnect.emit(),
51
+ });
52
+
53
+ private readonly _listeners = new Set<MessageListener>();
54
+ private readonly _protocol: Protocol;
55
+ private _ready = new Trigger();
56
+ private _ws?: WebSocket = undefined;
57
+ private _keepaliveCtx?: Context = undefined;
58
+ private _heartBeatContext?: Context = undefined;
59
+
60
+ constructor(
61
+ private _identityKey: string,
62
+ private _peerKey: string,
63
+ private readonly _config: MessengerConfig,
64
+ ) {
65
+ super();
66
+ this._protocol = this._config.protocol ?? protocol;
67
+ }
68
+
69
+ // TODO(burdon): Attach logging.
70
+ public get info() {
71
+ return {
72
+ open: this.isOpen,
73
+ identity: this._identityKey,
74
+ device: this._peerKey,
75
+ };
76
+ }
77
+
78
+ get identityKey() {
79
+ return this._identityKey;
80
+ }
81
+
82
+ get peerKey() {
83
+ return this._peerKey;
84
+ }
85
+
86
+ public get isOpen() {
87
+ return this._lifecycleState === LifecycleState.OPEN;
88
+ }
89
+
90
+ setIdentity({ peerKey, identityKey }: { peerKey: string; identityKey: string }) {
91
+ this._peerKey = peerKey;
92
+ this._identityKey = identityKey;
93
+ this._persistentLifecycle.scheduleRestart();
94
+ }
95
+
96
+ public addListener(listener: MessageListener): () => void {
97
+ this._listeners.add(listener);
98
+ return () => this._listeners.delete(listener);
99
+ }
100
+
101
+ /**
102
+ * Open connection to messaging service.
103
+ */
104
+ protected override async _open() {
105
+ log('opening...', { info: this.info });
106
+ this._persistentLifecycle.open().catch((err) => {
107
+ log.warn('Error while opening connection', { err });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Close connection and free resources.
113
+ */
114
+ protected override async _close() {
115
+ log('closing...', { peerKey: this._peerKey });
116
+ await this._persistentLifecycle.close();
117
+ }
118
+
119
+ private async _openWebSocket() {
120
+ const url = new URL(`/ws/${this._identityKey}/${this._peerKey}`, this._config.socketEndpoint);
121
+ this._ws = new WebSocket(url);
122
+
123
+ this._ws.onopen = () => {
124
+ log('opened', this.info);
125
+ this._ready.wake();
126
+ };
127
+ this._ws.onclose = () => {
128
+ log('closed', this.info);
129
+ this._persistentLifecycle.scheduleRestart();
130
+ };
131
+ this._ws.onerror = (event) => {
132
+ log.warn('EdgeClient socket error', { error: event.error, info: event.message });
133
+ this._persistentLifecycle.scheduleRestart();
134
+ };
135
+ /**
136
+ * https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
137
+ */
138
+ this._ws.onmessage = async (event) => {
139
+ if (event.data === '__pong__') {
140
+ this._onHeartbeat();
141
+ return;
142
+ }
143
+ const data = await toUint8Array(event.data);
144
+ const message = buf.fromBinary(MessageSchema, data);
145
+ log('received', { peerKey: this._peerKey, payload: protocol.getPayloadType(message) });
146
+ if (message) {
147
+ for (const listener of this._listeners) {
148
+ try {
149
+ await listener(message);
150
+ } catch (err) {
151
+ log.error('processing', { err, payload: protocol.getPayloadType(message) });
152
+ }
153
+ }
154
+ }
155
+ };
156
+
157
+ await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
158
+ this._keepaliveCtx = new Context();
159
+ scheduleTaskInterval(
160
+ this._keepaliveCtx,
161
+ async () => {
162
+ // TODO(mykola): use RFC6455 ping/pong once implemented in the browser?
163
+ // Cloudflare's worker responds to this `without interrupting hibernation`. https://developers.cloudflare.com/durable-objects/api/websockets/#setwebsocketautoresponse
164
+ this._ws?.send('__ping__');
165
+ },
166
+ SIGNAL_KEEPALIVE_INTERVAL,
167
+ );
168
+ this._ws.send('__ping__');
169
+ this._onHeartbeat();
170
+ }
171
+
172
+ private async _closeWebSocket() {
173
+ if (!this._ws) {
174
+ return;
175
+ }
176
+ try {
177
+ this._ready.throw(new WebsocketClosedError());
178
+ this._ready.reset();
179
+ void this._keepaliveCtx?.dispose();
180
+ this._keepaliveCtx = undefined;
181
+ void this._heartBeatContext?.dispose();
182
+ this._heartBeatContext = undefined;
183
+
184
+ // NOTE: Remove event handlers to avoid scheduling restart.
185
+ this._ws.onopen = () => {};
186
+ this._ws.onclose = () => {};
187
+ this._ws.onerror = () => {};
188
+ this._ws.close();
189
+ this._ws = undefined;
190
+ } catch (err) {
191
+ if (err instanceof Error && err.message.includes('WebSocket is closed before the connection is established.')) {
192
+ return;
193
+ }
194
+ log.warn('Error closing websocket', { err });
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Send message.
200
+ * NOTE: The message is guaranteed to be delivered but the service must respond with a message to confirm processing.
201
+ */
202
+ public async send(message: Message): Promise<void> {
203
+ if (this._ready.state !== TriggerState.RESOLVED) {
204
+ await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
205
+ }
206
+ invariant(this._ws);
207
+ invariant(!message.source || message.source.peerKey === this._peerKey);
208
+ log('sending...', { peerKey: this._peerKey, payload: protocol.getPayloadType(message) });
209
+ this._ws.send(buf.toBinary(MessageSchema, message));
210
+ }
211
+
212
+ private _onHeartbeat() {
213
+ if (this._lifecycleState !== LifecycleState.OPEN) {
214
+ return;
215
+ }
216
+ void this._heartBeatContext?.dispose();
217
+ this._heartBeatContext = new Context();
218
+ scheduleTask(
219
+ this._heartBeatContext,
220
+ () => {
221
+ this._persistentLifecycle.scheduleRestart();
222
+ },
223
+ 2 * SIGNAL_KEEPALIVE_INTERVAL,
224
+ );
225
+ }
226
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export class WebsocketClosedError extends Error {
6
+ constructor() {
7
+ super('WebSocket connection closed');
8
+ }
9
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,6 @@
4
4
 
5
5
  export * from '@dxos/protocols/buf/dxos/edge/messenger_pb';
6
6
 
7
- export * from './client';
7
+ export * from './edge-client';
8
8
  export * from './defs';
9
9
  export * from './protocol';
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { sleep, Trigger } from '@dxos/async';
8
+ import { log } from '@dxos/log';
9
+ import { describe, openAndClose, test } from '@dxos/test';
10
+
11
+ import { PersistentLifecycle } from './persistent-lifecycle';
12
+
13
+ describe('ConnectionState', () => {
14
+ test('first reconnect fires immediately', async () => {
15
+ const triggerCall = new Trigger<number>();
16
+ const persistentLifecycle = new PersistentLifecycle({
17
+ start: async () => {
18
+ triggerCall.wake(Date.now());
19
+ },
20
+ stop: async () => {},
21
+ });
22
+ await openAndClose(persistentLifecycle);
23
+
24
+ const triggerTimestamp = Date.now();
25
+ persistentLifecycle.scheduleRestart();
26
+ const timeToTrigger = (await triggerCall.wait({ timeout: 1000 })) - triggerTimestamp;
27
+ expect(timeToTrigger).to.be.lessThan(50);
28
+ });
29
+
30
+ test('second reconnect fires after 100ms', async () => {
31
+ let called = 0;
32
+ const triggerCall = new Trigger<number>();
33
+
34
+ const persistentLifecycle = new PersistentLifecycle({
35
+ start: async () => {
36
+ called += 1;
37
+ log.info('called', { called });
38
+ if (called < 3) {
39
+ throw new Error('TEST ERROR');
40
+ }
41
+ triggerCall.wake(Date.now());
42
+ },
43
+ stop: async () => {},
44
+ });
45
+
46
+ await openAndClose(persistentLifecycle);
47
+
48
+ const triggerTimestamp = Date.now();
49
+ await sleep(10);
50
+ const timeToTrigger = (await triggerCall.wait({ timeout: 1000 })) - triggerTimestamp;
51
+ expect(timeToTrigger).to.be.greaterThanOrEqual(100);
52
+ });
53
+
54
+ test('finish `restart` before close', async () => {
55
+ let restarted = false;
56
+ const persistentLifecycle = new PersistentLifecycle({
57
+ start: async () => await sleep(100),
58
+ stop: async () => {},
59
+ onRestart: async () => {
60
+ restarted = true;
61
+ },
62
+ });
63
+
64
+ await persistentLifecycle.open();
65
+
66
+ persistentLifecycle.scheduleRestart();
67
+ await sleep(10);
68
+ await persistentLifecycle.close();
69
+ expect(restarted).to.be.true;
70
+ });
71
+ });