@ledgerhq/live-network 3.0.0 → 3.1.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/README.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # live-network
2
2
 
3
- > [!NOTE]
4
- > **Status: STABLE** — Production-ready; API is considered stable.
3
+ > [!WARNING]
4
+ > **Status: DEPRECATED**
5
+ > Inside Ledger Live, use [RTK Query](https://redux-toolkit.js.org/rtk-query/overview) instead.
6
+ > Outside Ledger Live, use plain [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch)
7
+ > or [`axios`](https://axios-http.com/) directly.
8
+ > This package was extracted to ts-libs to unblock the migration of its dependents, not because it is
9
+ > the recommended path. No new consumers, please.
5
10
 
6
11
  `@ledgerhq/live-network` is the shared network layer for Ledger Live. It wraps the underlying HTTP client with Ledger-specific logging, error normalisation, and request optimisation utilities so all services in the monorepo make network calls consistently.
7
12
 
package/lib/state.d.ts CHANGED
@@ -6,9 +6,7 @@ type NetworkState = {
6
6
  getCallsRetry: number;
7
7
  };
8
8
  declare global {
9
- interface GlobalThis {
10
- __ledgerLiveNetworkState?: NetworkState;
11
- }
9
+ var __ledgerLiveNetworkState: NetworkState | undefined;
12
10
  }
13
11
  export declare function getNetworkState(): Readonly<NetworkState>;
14
12
  export type { NetworkState };
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,UAAU;QAClB,wBAAwB,CAAC,EAAE,YAAY,CAAC;KACzC;CACF;AAUD,wBAAgB,eAAe,IAAI,QAAQ,CAAC,YAAY,CAAC,CAGxD;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,wBAAwB,EAAE,YAAY,GAAG,SAAS,CAAC;CACxD;AAUD,wBAAgB,eAAe,IAAI,QAAQ,CAAC,YAAY,CAAC,CAGxD;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
package/lib/state.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":";;;AAcA,MAAM,QAAQ,GAAiB;IAC7B,iBAAiB,EAAE,KAAK;IACxB,iBAAiB,EAAE,KAAK;IACxB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE,GAAG,IAAI;IAC1B,aAAa,EAAE,CAAC;CACjB,CAAC;AAEF;IACE,UAAU,CAAC,wBAAwB,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;IACxD,OAAO,UAAU,CAAC,wBAAwB,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":";;;AAYA,MAAM,QAAQ,GAAiB;IAC7B,iBAAiB,EAAE,KAAK;IACxB,iBAAiB,EAAE,KAAK;IACxB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE,GAAG,IAAI;IAC1B,aAAa,EAAE,CAAC;CACjB,CAAC;AAEF;IACE,UAAU,CAAC,wBAAwB,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;IACxD,OAAO,UAAU,CAAC,wBAAwB,CAAC;AAC7C,CAAC"}
package/lib-es/state.d.ts CHANGED
@@ -6,9 +6,7 @@ type NetworkState = {
6
6
  getCallsRetry: number;
7
7
  };
8
8
  declare global {
9
- interface GlobalThis {
10
- __ledgerLiveNetworkState?: NetworkState;
11
- }
9
+ var __ledgerLiveNetworkState: NetworkState | undefined;
12
10
  }
13
11
  export declare function getNetworkState(): Readonly<NetworkState>;
14
12
  export type { NetworkState };
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,UAAU;QAClB,wBAAwB,CAAC,EAAE,YAAY,CAAC;KACzC;CACF;AAUD,wBAAgB,eAAe,IAAI,QAAQ,CAAC,YAAY,CAAC,CAGxD;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,wBAAwB,EAAE,YAAY,GAAG,SAAS,CAAC;CACxD;AAUD,wBAAgB,eAAe,IAAI,QAAQ,CAAC,YAAY,CAAC,CAGxD;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAcA,MAAM,QAAQ,GAAiB;IAC7B,iBAAiB,EAAE,KAAK;IACxB,iBAAiB,EAAE,KAAK;IACxB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE,GAAG,IAAI;IAC1B,aAAa,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,UAAU,eAAe;IAC7B,UAAU,CAAC,wBAAwB,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;IACxD,OAAO,UAAU,CAAC,wBAAwB,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAYA,MAAM,QAAQ,GAAiB;IAC7B,iBAAiB,EAAE,KAAK;IACxB,iBAAiB,EAAE,KAAK;IACxB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE,GAAG,IAAI;IAC1B,aAAa,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,UAAU,eAAe;IAC7B,UAAU,CAAC,wBAAwB,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;IACxD,OAAO,UAAU,CAAC,wBAAwB,CAAC;AAC7C,CAAC"}
package/package.json CHANGED
@@ -1,98 +1,131 @@
1
1
  {
2
2
  "name": "@ledgerhq/live-network",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Ledger Live network and cache utilities",
5
5
  "keywords": [
6
6
  "Ledger"
7
7
  ],
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/LedgerHQ/ledger-live.git"
11
- },
8
+ "homepage": "https://github.com/LedgerHQ/ts-libs/tree/main/libs/live-network",
12
9
  "bugs": {
13
- "url": "https://github.com/LedgerHQ/ledger-live/issues"
10
+ "url": "https://github.com/LedgerHQ/ts-libs/issues"
14
11
  },
15
- "homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/network",
16
- "publishConfig": {
17
- "access": "public"
12
+ "license": "Apache-2.0",
13
+ "files": [
14
+ "lib",
15
+ "lib-es",
16
+ "src",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/LedgerHQ/ts-libs.git"
18
22
  },
23
+ "main": "lib/index.js",
24
+ "module": "lib-es/index.js",
25
+ "types": "lib/index.d.ts",
19
26
  "typesVersions": {
20
27
  "*": {
28
+ "network": [
29
+ "lib/network.d.ts"
30
+ ],
31
+ "cache": [
32
+ "lib/cache.d.ts"
33
+ ],
34
+ "errors": [
35
+ "lib/errors.d.ts"
36
+ ],
37
+ "batcher": [
38
+ "lib/batcher/index.d.ts"
39
+ ],
40
+ "batcher/types": [
41
+ "lib/batcher/types.d.ts"
42
+ ],
43
+ "batcher/index": [
44
+ "lib/batcher/index.d.ts"
45
+ ],
21
46
  "lib/*": [
22
47
  "lib/*"
23
48
  ],
24
49
  "lib-es/*": [
25
50
  "lib-es/*"
26
- ],
27
- "batcher": [
28
- "lib/batcher/index"
29
- ],
30
- "*": [
31
- "lib/*",
32
- "lib/batcher/*"
33
51
  ]
34
52
  }
35
53
  },
36
54
  "exports": {
37
- "./lib/*": "./lib/*.js",
38
- "./lib/*.js": "./lib/*.js",
39
- "./lib-es/*": "./lib-es/*.js",
40
- "./lib-es/*.js": "./lib-es/*.js",
55
+ ".": {
56
+ "@ledgerhq/source": "./src/index.ts",
57
+ "import": "./lib-es/index.js",
58
+ "require": "./lib/index.js",
59
+ "default": "./lib/index.js"
60
+ },
61
+ "./network": {
62
+ "@ledgerhq/source": "./src/network.ts",
63
+ "import": "./lib-es/network.js",
64
+ "require": "./lib/network.js",
65
+ "default": "./lib/network.js"
66
+ },
67
+ "./cache": {
68
+ "@ledgerhq/source": "./src/cache.ts",
69
+ "import": "./lib-es/cache.js",
70
+ "require": "./lib/cache.js",
71
+ "default": "./lib/cache.js"
72
+ },
73
+ "./errors": {
74
+ "@ledgerhq/source": "./src/errors.ts",
75
+ "import": "./lib-es/errors.js",
76
+ "require": "./lib/errors.js",
77
+ "default": "./lib/errors.js"
78
+ },
41
79
  "./batcher": {
42
80
  "@ledgerhq/source": "./src/batcher/index.ts",
81
+ "import": "./lib-es/batcher/index.js",
43
82
  "require": "./lib/batcher/index.js",
44
- "default": "./lib-es/batcher/index.js"
83
+ "default": "./lib/batcher/index.js"
45
84
  },
46
- "./batcher/*": {
47
- "@ledgerhq/source": "./src/batcher/*.ts",
48
- "require": "./lib/batcher/*.js",
49
- "default": "./lib-es/batcher/*.js"
85
+ "./batcher/types": {
86
+ "@ledgerhq/source": "./src/batcher/types.ts",
87
+ "import": "./lib-es/batcher/types.js",
88
+ "require": "./lib/batcher/types.js",
89
+ "default": "./lib/batcher/types.js"
50
90
  },
51
- "./*": {
52
- "@ledgerhq/source": "./src/*.ts",
53
- "require": "./lib/*.js",
54
- "default": "./lib-es/*.js"
55
- },
56
- "./*.js": {
57
- "@ledgerhq/source": "./src/*.ts",
58
- "require": "./lib/*.js",
59
- "default": "./lib-es/*.js"
60
- },
61
- ".": {
62
- "@ledgerhq/source": "./src/index.ts",
63
- "require": "./lib/index.js",
64
- "default": "./lib-es/index.js"
91
+ "./batcher/index": {
92
+ "@ledgerhq/source": "./src/batcher/index.ts",
93
+ "import": "./lib-es/batcher/index.js",
94
+ "require": "./lib/batcher/index.js",
95
+ "default": "./lib/batcher/index.js"
65
96
  },
97
+ "./lib/*": "./lib/*.js",
98
+ "./lib-es/*": "./lib-es/*.js",
66
99
  "./package.json": "./package.json"
67
100
  },
68
- "license": "Apache-2.0",
101
+ "publishConfig": {
102
+ "access": "public"
103
+ },
69
104
  "dependencies": {
70
105
  "lru-cache": "^7.14.1",
71
- "@ledgerhq/live-promise": "^0.3.0",
72
- "@ledgerhq/logs": "^6.17.0"
106
+ "@ledgerhq/live-promise": "0.4.0",
107
+ "@ledgerhq/logs": "6.19.0"
73
108
  },
74
109
  "peerDependencies": {
75
110
  "axios": "1.13.5"
76
111
  },
77
112
  "devDependencies": {
78
- "@types/invariant": "^2.2.35",
113
+ "@swc/core": "1.16.1",
114
+ "@swc/jest": "0.2.39",
79
115
  "@types/jest": "30.0.0",
80
- "@types/node": "24.12.0",
116
+ "@types/node": "26.4.0",
81
117
  "axios": "1.13.5",
82
- "jest": "30.2.0",
83
- "@swc/jest": "0.2.39",
84
- "@swc/core": "1.15.11"
118
+ "jest": "30.5.0",
119
+ "jest-sonar": "0.2.16",
120
+ "oxlint": "1.80.0",
121
+ "typescript": "7.0.2"
85
122
  },
86
123
  "scripts": {
87
- "clean": "rimraf lib lib-es",
124
+ "clean": "rm -rf lib lib-es",
88
125
  "build": "tsc --project tsconfig.build.json && tsc --project tsconfig.build.json -m esnext --moduleResolution bundler --outDir lib-es",
89
- "prewatch": "pnpm build",
90
- "watch": "tsc --watch --project tsconfig.build.json",
91
- "watch:es": "tsc --watch --project tsconfig.build.json -m esnext --moduleResolution bundler --outDir lib-es",
92
- "lint": "oxlint -c ../oxc-live-libs/.oxlintrc.json ./src",
93
- "lint:fix": "pnpm lint --fix",
126
+ "lint": "oxlint ./src",
127
+ "typecheck": "tsc --noEmit",
94
128
  "test": "jest",
95
- "coverage": "jest --coverage",
96
- "unimported": "unimported"
129
+ "coverage": "jest --coverage"
97
130
  }
98
131
  }
@@ -0,0 +1,36 @@
1
+ import { makeBatcher } from "./index";
2
+
3
+ beforeEach(() => jest.useFakeTimers());
4
+ afterEach(() => jest.useRealTimers());
5
+
6
+ describe("makeBatcher", () => {
7
+ it("coalesces one tick of calls into a single request and fans the results out", async () => {
8
+ const request = jest.fn(async (inputs: number[], prefix: string) =>
9
+ inputs.map(i => `${prefix}${i}`),
10
+ );
11
+ const batcher = makeBatcher(request, "x");
12
+
13
+ const batch = Promise.all([batcher(1), batcher(2)]);
14
+ expect(request).not.toHaveBeenCalled();
15
+ await jest.advanceTimersByTimeAsync(0);
16
+ await expect(batch).resolves.toEqual(["x1", "x2"]);
17
+
18
+ const next = batcher(3);
19
+ await jest.advanceTimersByTimeAsync(0);
20
+ await expect(next).resolves.toBe("x3");
21
+ expect(request.mock.calls).toEqual([
22
+ [[1, 2], "x"],
23
+ [[3], "x"],
24
+ ]);
25
+ });
26
+
27
+ it("rejects every waiter of a failed batch", async () => {
28
+ const error = new Error("down");
29
+ const batcher = makeBatcher<number, null, number>(() => Promise.reject(error), null);
30
+
31
+ const settled = Promise.all([batcher(1), batcher(2)].map(p => p.catch(e => e)));
32
+ await jest.advanceTimersByTimeAsync(0);
33
+
34
+ await expect(settled).resolves.toEqual([error, error]);
35
+ });
36
+ });
@@ -0,0 +1,67 @@
1
+ import { hours, makeLRUCache, minutes, seconds } from "./cache";
2
+
3
+ it("builds ttl options", () => {
4
+ expect([seconds(30), seconds(1, 5), minutes(2), hours(2), hours(1, 3)]).toEqual([
5
+ { max: 100, ttl: 30_000 },
6
+ { max: 5, ttl: 1_000 },
7
+ { max: 100, ttl: 120_000 },
8
+ { max: 100, ttl: 7_200_000 },
9
+ { max: 3, ttl: 3_600_000 },
10
+ ]);
11
+ });
12
+
13
+ describe("makeLRUCache", () => {
14
+ let calls = 0;
15
+ const f = jest.fn(async (k: string) => `${k}${++calls}`);
16
+
17
+ beforeEach(() => {
18
+ calls = 0;
19
+ f.mockClear();
20
+ });
21
+
22
+ it("memoizes per extracted key and invalidates on demand", async () => {
23
+ const cached = makeLRUCache(f, k => k);
24
+
25
+ await expect(cached("a")).resolves.toBe("a1");
26
+ await expect(cached("a")).resolves.toBe("a1");
27
+ await expect(cached("b")).resolves.toBe("b2");
28
+ await expect(cached.force("a")).resolves.toBe("a3");
29
+ await expect(cached("a")).resolves.toBe("a3");
30
+
31
+ cached.hydrate("a", "hydrated");
32
+ await expect(cached("a")).resolves.toBe("hydrated");
33
+
34
+ cached.clear("a");
35
+ await expect(cached("a")).resolves.toBe("a4");
36
+ await expect(cached("b")).resolves.toBe("b2");
37
+
38
+ cached.reset();
39
+ await expect(cached("b")).resolves.toBe("b5");
40
+ expect(f).toHaveBeenCalledTimes(5);
41
+ });
42
+
43
+ it("collapses concurrent calls onto one promise, and uses one key without an extractor", () => {
44
+ const cached = makeLRUCache(f);
45
+
46
+ expect(cached("a")).toBe(cached("b"));
47
+ expect(f).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ it.each([false, true])("does not memoize a rejection (force: %s)", async force => {
51
+ const rejectingOnce = jest.fn<Promise<string>, [string]>();
52
+ rejectingOnce.mockRejectedValueOnce(new Error("boom")).mockResolvedValue("ok");
53
+ const cached = makeLRUCache(rejectingOnce, k => k);
54
+
55
+ await expect(force ? cached.force("a") : cached("a")).rejects.toThrow("boom");
56
+ await expect(cached("a")).resolves.toBe("ok");
57
+ });
58
+
59
+ it("defaults to ttlAutopurge when no max is given", async () => {
60
+ const cached = makeLRUCache(f, k => k, { ttl: 20 });
61
+
62
+ await expect(cached("a")).resolves.toBe("a1");
63
+ await expect(cached("a")).resolves.toBe("a1");
64
+ await new Promise(resolve => setTimeout(resolve, 40));
65
+ await expect(cached("a")).resolves.toBe("a2");
66
+ });
67
+ });
@@ -13,7 +13,7 @@ describe("live-network error classes", () => {
13
13
  });
14
14
  it("assigns extra fields", () => {
15
15
  const e = new LedgerAPI4xx("err", { status: 404 });
16
- expect((e as Record<string, unknown>).status).toBe(404);
16
+ expect(e).toHaveProperty("status", 404);
17
17
  });
18
18
  });
19
19
 
@@ -29,7 +29,7 @@ describe("live-network error classes", () => {
29
29
  });
30
30
  it("assigns extra fields", () => {
31
31
  const e = new LedgerAPI5xx("err", { status: 503 });
32
- expect((e as Record<string, unknown>).status).toBe(503);
32
+ expect(e).toHaveProperty("status", 503);
33
33
  });
34
34
  });
35
35
 
@@ -43,5 +43,9 @@ describe("live-network error classes", () => {
43
43
  it("accepts a custom message", () => {
44
44
  expect(new NetworkDown("timeout").message).toBe("timeout");
45
45
  });
46
+ it("assigns extra fields", () => {
47
+ const e = new NetworkDown("down", { url: "/v1/thing" });
48
+ expect(e).toHaveProperty("url", "/v1/thing");
49
+ });
46
50
  });
47
51
  });
@@ -0,0 +1,20 @@
1
+ import network, {
2
+ getNetworkState,
3
+ LedgerAPI4xx,
4
+ LedgerAPI5xx,
5
+ NetworkDown,
6
+ setNetworkState,
7
+ } from "./index";
8
+ import { newImplementation } from "./network";
9
+
10
+ jest.mock("axios");
11
+
12
+ it("re-exports the public API", () => {
13
+ expect(network).toBe(newImplementation);
14
+ expect([getNetworkState(), setNetworkState({})]).toEqual([expect.any(Object), undefined]);
15
+ expect([LedgerAPI4xx, LedgerAPI5xx, NetworkDown].map(E => new E().name)).toEqual([
16
+ "LedgerAPI4xx",
17
+ "LedgerAPI5xx",
18
+ "NetworkDown",
19
+ ]);
20
+ });
@@ -1,6 +1,14 @@
1
1
  import axios, { AxiosHeaders } from "axios";
2
2
  import { getNetworkState } from "./state";
3
- import network, { requestInterceptor, responseInterceptor, setNetworkState } from "./network";
3
+ import network, {
4
+ errorInterceptor,
5
+ newImplementation,
6
+ requestInterceptor,
7
+ responseInterceptor,
8
+ setNetworkState,
9
+ } from "./network";
10
+ import type { LiveNetworkRequest } from "./network";
11
+ import { LedgerAPI4xx, LedgerAPI5xx } from "./errors";
4
12
  import * as logs from "@ledgerhq/logs";
5
13
 
6
14
  jest.mock("axios");
@@ -192,3 +200,177 @@ describe("network", () => {
192
200
  });
193
201
  });
194
202
  });
203
+
204
+ type InterceptedError = Parameters<typeof errorInterceptor>[0];
205
+
206
+ const PRISTINE = { ...getNetworkState() };
207
+
208
+ const asError = (props: Partial<InterceptedError> = {}): InterceptedError =>
209
+ Object.assign(
210
+ new Error("failed"),
211
+ { isAxiosError: true, toJSON: () => ({}), cause: undefined },
212
+ props,
213
+ );
214
+
215
+ const failing = (
216
+ status: number,
217
+ data: unknown,
218
+ config: { baseURL?: string; method?: string; metadata?: { startTime: number } } = {},
219
+ ): InterceptedError =>
220
+ asError({
221
+ response: {
222
+ status,
223
+ statusText: "",
224
+ data,
225
+ headers: {},
226
+ config: { url: "/u", method: "get", headers: new AxiosHeaders(), ...config },
227
+ },
228
+ });
229
+
230
+ const settle = async (promise: Promise<unknown>): Promise<unknown> => {
231
+ const settled = promise.catch(e => e);
232
+ await jest.runAllTimersAsync();
233
+ return settled;
234
+ };
235
+
236
+ describe("interceptors", () => {
237
+ afterEach(() => {
238
+ jest.clearAllMocks();
239
+ setNetworkState(PRISTINE);
240
+ });
241
+
242
+ it("rethrows an error carrying no response config", () => {
243
+ const error = asError();
244
+ expect(() => errorInterceptor(error)).toThrow(error);
245
+ });
246
+
247
+ it.each<[number, jest.Constructable]>([
248
+ [404, LedgerAPI4xx],
249
+ [500, LedgerAPI5xx],
250
+ [0, LedgerAPI5xx],
251
+ ])("maps status %i to the matching typed error", (status, Expected) => {
252
+ expect(() => errorInterceptor(failing(status, { message: "m" }))).toThrow(Expected);
253
+ });
254
+
255
+ it.each<[unknown, string]>([
256
+ [{ message: "m" }, "m"],
257
+ [{ error_message: "em" }, "em"],
258
+ [{ error: "e" }, "e"],
259
+ [{ msg: "s" }, "s"],
260
+ [{ errors: [{ message: "nested" }] }, "nested"],
261
+ [{ errors: ["plain"] }, "plain"],
262
+ [{ errors: [] }, "API HTTP 400 /u"],
263
+ [null, "API HTTP 400 /u"],
264
+ [{ unknown: 1 }, "API HTTP 400 /u"],
265
+ [JSON.stringify({ message: JSON.stringify({ message: "deep" }) }), "deep"],
266
+ [
267
+ JSON.stringify({ message: `JsDefined(${JSON.stringify({ message: "wrapped" })})` }),
268
+ "wrapped",
269
+ ],
270
+ [JSON.stringify({ message: JSON.stringify({ message: { message: "twice" } }) }), "twice"],
271
+ [JSON.stringify([{ message: JSON.stringify({ message: "first" }) }]), "first"],
272
+ [JSON.stringify({ message: '{"code":1}' }), '{"code":1}'],
273
+ [JSON.stringify({ message: { notAString: true } }), "API HTTP 400 /u"],
274
+ [JSON.stringify({ message: JSON.stringify({ message: "" }) }), "API HTTP 400 /u"],
275
+ ["<html>not json</html>", "API HTTP 400 /u"],
276
+ ])("derives the message of %j", (data, message) => {
277
+ expect(() => errorInterceptor(failing(400, data))).toThrow(message);
278
+ });
279
+
280
+ it("logs the failure, including the body only when debugHttpResponse is on", () => {
281
+ const spy = jest.spyOn(logs, "log");
282
+ const data = { message: "boom" };
283
+ const throwing = () =>
284
+ errorInterceptor(
285
+ failing(500, data, {
286
+ baseURL: "https://api",
287
+ method: undefined,
288
+ metadata: { startTime: Date.now() - 1234 },
289
+ }),
290
+ );
291
+
292
+ expect(throwing).toThrow("boom");
293
+ expect(spy).toHaveBeenLastCalledWith(
294
+ "network-error",
295
+ expect.stringMatching(/^500 {2}https:\/\/api\/u \(1[0-9]{3}ms\): boom$/),
296
+ {},
297
+ );
298
+
299
+ setNetworkState({ debugHttpResponse: true });
300
+ expect(throwing).toThrow("boom");
301
+ expect(spy).toHaveBeenLastCalledWith("network-error", expect.any(String), { data });
302
+ });
303
+
304
+ it("logs a request and its response without a baseURL", () => {
305
+ const spy = jest.spyOn(logs, "log");
306
+ setNetworkState({ enableNetworkLogs: true });
307
+ const config = { url: "/u", headers: new AxiosHeaders() };
308
+
309
+ requestInterceptor(config);
310
+ responseInterceptor({ config, data: "d", status: 200, statusText: "", headers: {} });
311
+
312
+ expect(spy.mock.calls.map(call => call[1])).toEqual([
313
+ " /u",
314
+ expect.stringMatching(/^200 undefined \/u \(\d+ms\)$/),
315
+ ]);
316
+ });
317
+
318
+ it("never sets a User-Agent header outside of node", () => {
319
+ Object.defineProperty(globalThis, "window", { value: {}, configurable: true });
320
+
321
+ setNetworkState({ ledgerClientVersion: "web/1" });
322
+ expect(axios.defaults.headers.common).toMatchObject({ "X-Ledger-Client-Version": "web/1" });
323
+ expect(axios.defaults.headers.common["User-Agent"]).toBeUndefined();
324
+
325
+ setNetworkState({ ledgerClientVersion: "" });
326
+ expect(axios.defaults.headers.common["X-Ledger-Client-Version"]).toBeUndefined();
327
+
328
+ Object.defineProperty(globalThis, "window", { value: undefined, configurable: true });
329
+ });
330
+ });
331
+
332
+ describe("request implementations", () => {
333
+ beforeEach(() => jest.useFakeTimers());
334
+
335
+ afterEach(() => {
336
+ jest.useRealTimers();
337
+ jest.clearAllMocks();
338
+ setNetworkState(PRISTINE);
339
+ });
340
+
341
+ it("defaults to GET with the configured timeout and returns the response essentials", async () => {
342
+ setNetworkState({ getCallsTimeout: 42 });
343
+ const headers = new AxiosHeaders();
344
+ mockedAxios.mockResolvedValue({ data: "d", status: 200, headers });
345
+
346
+ await expect(newImplementation({ url: "/u" })).resolves.toEqual({
347
+ data: "d",
348
+ status: 200,
349
+ headers,
350
+ });
351
+ expect(mockedAxios).toHaveBeenCalledWith({ url: "/u", method: "GET", timeout: 42 });
352
+ });
353
+
354
+ it.each<[LiveNetworkRequest<never>, unknown, number]>([
355
+ [{ method: "POST" }, new Error("x"), 1],
356
+ [{ method: "GET", timeout: 1 }, { status: 404 }, 1],
357
+ [{ method: "GET", timeout: 1 }, { status: 503 }, 3],
358
+ [{ method: "GET", timeout: 1 }, new Error("x"), 3],
359
+ ])("issues %j on %j exactly %i time(s)", async (request, error, calls) => {
360
+ mockedAxios.mockRejectedValue(error);
361
+
362
+ await expect(settle(newImplementation({ url: "/u", ...request }))).resolves.toBe(error);
363
+ expect(mockedAxios).toHaveBeenCalledTimes(calls);
364
+ });
365
+
366
+ it.each<["GET" | "POST", number]>([
367
+ ["POST", 1],
368
+ ["GET", 3],
369
+ ])("retries a status-less %s through the deprecated export %i time(s)", async (method, calls) => {
370
+ const error = new Error("x");
371
+ mockedAxios.mockRejectedValue(error);
372
+
373
+ await expect(settle(network({ url: "/u", method, timeout: 1 }))).resolves.toBe(error);
374
+ expect(mockedAxios).toHaveBeenCalledTimes(calls);
375
+ });
376
+ });
package/src/state.ts CHANGED
@@ -7,9 +7,7 @@ type NetworkState = {
7
7
  };
8
8
 
9
9
  declare global {
10
- interface GlobalThis {
11
- __ledgerLiveNetworkState?: NetworkState;
12
- }
10
+ var __ledgerLiveNetworkState: NetworkState | undefined;
13
11
  }
14
12
 
15
13
  const defaults: NetworkState = {
@@ -1,14 +0,0 @@
1
- {
2
- "entry": [
3
- "src/batcher/index.ts",
4
- "src/batcher/types.ts",
5
- "src/cache.ts",
6
- "src/index.ts",
7
- "src/network.ts"
8
- ],
9
- "ignoreUnimported": [],
10
- "ignoreUnresolved": [
11
- "@jest/expect-utils",
12
- "@jest/get-type",
13
- "jest-util"]
14
- }
package/LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- The MIT License
2
-
3
- Copyright (c) 2017-present Ledger https://www.ledger.com/
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
package/jest.config.js DELETED
@@ -1,20 +0,0 @@
1
- module.exports = {
2
- testEnvironment: "node",
3
- transform: {
4
- "^.+\\.(t|j)sx?$": [
5
- "@swc/jest",
6
- {
7
- jsc: {
8
- target: "esnext",
9
- },
10
- },
11
- ],
12
- },
13
- testPathIgnorePatterns: ["lib/", "lib-es/"],
14
- coverageReporters: ["json", ["lcov", { file: "lcov.info", projectRoot: "../../" }], "text"],
15
- reporters: [
16
- "default",
17
- ...(process.env.CI ? ["github-actions"] : []),
18
- ["jest-sonar", { outputName: "sonar-executionTests-report.xml", reportedFilePath: "absolute" }],
19
- ],
20
- };