ExtensionStrategy.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import * as TE from "fp-ts/TaskEither"
  2. import * as O from "fp-ts/Option"
  3. import { pipe } from "fp-ts/function"
  4. import { AxiosRequestConfig } from "axios"
  5. import cloneDeep from "lodash/cloneDeep"
  6. import { NetworkResponse, NetworkStrategy } from "../network"
  7. import { browserIsChrome, browserIsFirefox } from "../utils/userAgent"
  8. export const hasExtensionInstalled = () =>
  9. typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined"
  10. export const hasChromeExtensionInstalled = () =>
  11. hasExtensionInstalled() && browserIsChrome()
  12. export const hasFirefoxExtensionInstalled = () =>
  13. hasExtensionInstalled() && browserIsFirefox()
  14. export const cancelRunningExtensionRequest = () => {
  15. window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRunningRequest()
  16. }
  17. export const defineSubscribableObject = <T extends object>(obj: T) => {
  18. const proxyObject = {
  19. ...obj,
  20. _subscribers: {} as {
  21. // eslint-disable-next-line no-unused-vars
  22. [key in keyof T]?: ((...args: any[]) => any)[]
  23. },
  24. subscribe(prop: keyof T, func: (...args: any[]) => any): void {
  25. if (Array.isArray(this._subscribers[prop])) {
  26. this._subscribers[prop]?.push(func)
  27. } else {
  28. this._subscribers[prop] = [func]
  29. }
  30. },
  31. }
  32. type SubscribableProxyObject = typeof proxyObject
  33. return new Proxy(proxyObject, {
  34. set(obj, prop, newVal) {
  35. obj[prop as keyof SubscribableProxyObject] = newVal
  36. const currentSubscribers = obj._subscribers[prop as keyof T]
  37. if (Array.isArray(currentSubscribers)) {
  38. for (const subscriber of currentSubscribers) {
  39. subscriber(newVal)
  40. }
  41. }
  42. return true
  43. },
  44. })
  45. }
  46. const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => {
  47. const reqClone = cloneDeep(req)
  48. // If the parameters are URLSearchParams, inject them to URL instead
  49. // This prevents marshalling issues with structured cloning of URLSearchParams
  50. if (reqClone.params instanceof URLSearchParams) {
  51. try {
  52. const url = new URL(reqClone.url ?? "")
  53. for (const [key, value] of reqClone.params.entries()) {
  54. url.searchParams.append(key, value)
  55. }
  56. reqClone.url = url.toString()
  57. } catch (e) {}
  58. reqClone.params = {}
  59. }
  60. return reqClone
  61. }
  62. const extensionStrategy: NetworkStrategy = (req) =>
  63. pipe(
  64. TE.Do,
  65. TE.bind("processedReq", () => TE.of(preProcessRequest(req))),
  66. // Storeing backup timing data in case the extension does not have that info
  67. TE.bind("backupTimeDataStart", () => TE.of(new Date().getTime())),
  68. // Run the request
  69. TE.bind("response", ({ processedReq }) =>
  70. pipe(
  71. window.__POSTWOMAN_EXTENSION_HOOK__,
  72. O.fromNullable,
  73. TE.fromOption(() => "NO_PW_EXT_HOOK" as const),
  74. TE.chain((extensionHook) =>
  75. TE.tryCatch(
  76. () =>
  77. extensionHook.sendRequest({
  78. ...processedReq,
  79. wantsBinary: true,
  80. }),
  81. (err) => err as any
  82. )
  83. )
  84. )
  85. ),
  86. // Inject backup time data if not present
  87. TE.map(({ backupTimeDataStart, response }) => ({
  88. ...response,
  89. config: {
  90. timeData: {
  91. startTime: backupTimeDataStart,
  92. endTime: new Date().getTime(),
  93. },
  94. ...response.config,
  95. },
  96. })),
  97. TE.orElse((e) =>
  98. e !== "cancellation" && e.response
  99. ? TE.right(e.response as NetworkResponse)
  100. : TE.left(e)
  101. )
  102. )
  103. export default extensionStrategy