@neteasecloudmusicapienhanced/api 4.31.0 → 4.32.1

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 (439) hide show
  1. package/LICENSE +21 -21
  2. package/README.MD +248 -251
  3. package/app.js +18 -18
  4. package/data/china_ip_ranges.txt +4147 -4147
  5. package/data/deviceid.txt +24641 -24641
  6. package/generateConfig.js +24 -24
  7. package/interface.d.ts +1843 -1836
  8. package/main.js +63 -63
  9. package/module/activate_init_profile.js +9 -9
  10. package/module/aidj_content_rcmd.js +28 -28
  11. package/module/album.js +6 -6
  12. package/module/album_detail.js +12 -12
  13. package/module/album_detail_dynamic.js +12 -12
  14. package/module/album_list.js +16 -16
  15. package/module/album_list_style.js +15 -15
  16. package/module/album_new.js +11 -11
  17. package/module/album_newest.js +6 -6
  18. package/module/album_privilege.js +9 -9
  19. package/module/album_songsaleboard.js +19 -19
  20. package/module/album_sub.js +10 -10
  21. package/module/album_sublist.js +11 -11
  22. package/module/api.js +21 -21
  23. package/module/artist_album.js +15 -15
  24. package/module/artist_desc.js +9 -9
  25. package/module/artist_detail.js +10 -10
  26. package/module/artist_detail_dynamic.js +9 -9
  27. package/module/artist_fans.js +11 -11
  28. package/module/artist_follow_count.js +13 -13
  29. package/module/artist_list.js +33 -33
  30. package/module/artist_mv.js +12 -12
  31. package/module/artist_new_mv.js +12 -12
  32. package/module/artist_new_song.js +12 -12
  33. package/module/artist_songs.js +12 -12
  34. package/module/artist_sub.js +11 -11
  35. package/module/artist_sublist.js +11 -11
  36. package/module/artist_top_song.js +8 -8
  37. package/module/artist_video.js +15 -15
  38. package/module/artists.js +6 -6
  39. package/module/audio_match.js +19 -19
  40. package/module/avatar_upload.js +22 -22
  41. package/module/banner.js +16 -16
  42. package/module/batch.js +12 -12
  43. package/module/broadcast_category_region_get.js +11 -11
  44. package/module/broadcast_channel_collect_list.js +12 -12
  45. package/module/broadcast_channel_currentinfo.js +13 -13
  46. package/module/broadcast_channel_list.js +13 -13
  47. package/module/broadcast_sub.js +12 -12
  48. package/module/calendar.js +8 -8
  49. package/module/captcha_sent.js +11 -11
  50. package/module/captcha_verify.js +11 -11
  51. package/module/cellphone_existence_check.js +10 -10
  52. package/module/check_music.js +30 -30
  53. package/module/cloud.js +164 -164
  54. package/module/cloud_import.js +39 -39
  55. package/module/cloud_lyric_get.js +11 -11
  56. package/module/cloud_match.js +13 -13
  57. package/module/cloud_upload_complete.js +72 -72
  58. package/module/cloud_upload_token.js +111 -111
  59. package/module/cloudsearch.js +13 -13
  60. package/module/comment.js +30 -30
  61. package/module/comment_album.js +16 -16
  62. package/module/comment_dj.js +16 -16
  63. package/module/comment_event.js +15 -15
  64. package/module/comment_floor.js +16 -16
  65. package/module/comment_hot.js +18 -18
  66. package/module/comment_hug_list.js +20 -20
  67. package/module/comment_info_list.js +30 -30
  68. package/module/comment_like.js +20 -20
  69. package/module/comment_music.js +16 -16
  70. package/module/comment_mv.js +16 -16
  71. package/module/comment_new.js +37 -37
  72. package/module/comment_playlist.js +16 -16
  73. package/module/{comment_delete.js → comment_report.js} +11 -10
  74. package/module/comment_video.js +16 -16
  75. package/module/countries_code_list.js +6 -6
  76. package/module/creator_authinfo_get.js +6 -6
  77. package/module/daily_signin.js +16 -16
  78. package/module/digitalAlbum_detail.js +13 -13
  79. package/module/digitalAlbum_ordering.js +22 -22
  80. package/module/digitalAlbum_purchased.js +15 -15
  81. package/module/digitalAlbum_sales.js +13 -13
  82. package/module/djRadio_top.js +15 -15
  83. package/module/dj_banner.js +6 -6
  84. package/module/dj_category_excludehot.js +10 -10
  85. package/module/dj_category_recommend.js +10 -10
  86. package/module/dj_catelist.js +6 -6
  87. package/module/dj_detail.js +9 -9
  88. package/module/dj_difm_all_style_channel.js +9 -9
  89. package/module/dj_difm_channel_subscribe.js +9 -9
  90. package/module/dj_difm_channel_unsubscribe.js +9 -9
  91. package/module/dj_difm_playing_tracks_list.js +11 -11
  92. package/module/dj_difm_subscribe_channels_get.js +13 -13
  93. package/module/dj_hot.js +10 -10
  94. package/module/dj_paygift.js +15 -15
  95. package/module/dj_personalize_recommend.js +12 -12
  96. package/module/dj_program.js +12 -12
  97. package/module/dj_program_detail.js +9 -9
  98. package/module/dj_program_toplist.js +10 -10
  99. package/module/dj_program_toplist_hours.js +13 -13
  100. package/module/dj_radio_hot.js +11 -11
  101. package/module/dj_recommend.js +6 -6
  102. package/module/dj_recommend_type.js +32 -32
  103. package/module/dj_sub.js +10 -10
  104. package/module/dj_sublist.js +11 -11
  105. package/module/dj_subscriber.js +12 -12
  106. package/module/dj_today_perfered.js +13 -13
  107. package/module/dj_toplist.js +14 -14
  108. package/module/dj_toplist_hours.js +10 -10
  109. package/module/dj_toplist_newcomer.js +9 -9
  110. package/module/dj_toplist_pay.js +9 -9
  111. package/module/dj_toplist_popular.js +10 -10
  112. package/module/eapi_decrypt.js +27 -27
  113. package/module/event.js +10 -10
  114. package/module/event_del.js +9 -9
  115. package/module/event_forward.js +11 -11
  116. package/module/fanscenter_basicinfo_age_get.js +6 -6
  117. package/module/fanscenter_basicinfo_gender_get.js +10 -10
  118. package/module/fanscenter_basicinfo_province_get.js +10 -10
  119. package/module/fanscenter_overview_get.js +6 -6
  120. package/module/fanscenter_trend_list.js +10 -10
  121. package/module/fm_trash.js +11 -11
  122. package/module/follow.js +11 -11
  123. package/module/get_userids.js +7 -7
  124. package/module/history_recommend_songs.js +11 -11
  125. package/module/history_recommend_songs_detail.js +13 -13
  126. package/module/homepage_block_page.js +8 -8
  127. package/module/homepage_dragon_ball.js +10 -10
  128. package/module/hot_topic.js +10 -10
  129. package/module/hug_comment.js +16 -16
  130. package/module/inner_version.js +16 -16
  131. package/module/like.js +13 -13
  132. package/module/likelist.js +9 -9
  133. package/module/listen_data_realtime_report.js +11 -11
  134. package/module/listen_data_report.js +12 -12
  135. package/module/listen_data_today_song.js +9 -9
  136. package/module/listen_data_total.js +9 -9
  137. package/module/listen_data_year_report.js +9 -9
  138. package/module/listentogether_accept.js +13 -13
  139. package/module/listentogether_end.js +9 -9
  140. package/module/listentogether_heatbeat.js +12 -12
  141. package/module/listentogether_play_command.js +21 -21
  142. package/module/listentogether_room_check.js +9 -9
  143. package/module/listentogether_room_create.js +9 -9
  144. package/module/listentogether_status.js +10 -10
  145. package/module/listentogether_sync_list_command.js +26 -26
  146. package/module/listentogether_sync_playlist_get.js +13 -13
  147. package/module/login.js +41 -41
  148. package/module/login_cellphone.js +40 -40
  149. package/module/login_qr_check.js +29 -29
  150. package/module/login_qr_create.js +30 -30
  151. package/module/login_qr_key.js +19 -19
  152. package/module/login_refresh.js +21 -21
  153. package/module/login_status.js +21 -21
  154. package/module/logout.js +6 -6
  155. package/module/lyric.js +14 -14
  156. package/module/lyric_new.js +17 -17
  157. package/module/mlog_music_rcmd.js +13 -13
  158. package/module/mlog_to_video.js +13 -13
  159. package/module/mlog_url.js +11 -11
  160. package/module/msg_comments.js +17 -17
  161. package/module/msg_forwards.js +11 -11
  162. package/module/msg_notices.js +10 -10
  163. package/module/msg_private.js +11 -11
  164. package/module/msg_private_history.js +12 -12
  165. package/module/msg_recentcontact.js +11 -11
  166. package/module/music_first_listen_info.js +13 -13
  167. package/module/musician_cloudbean.js +7 -7
  168. package/module/musician_cloudbean_obtain.js +14 -14
  169. package/module/musician_data_overview.js +11 -11
  170. package/module/musician_play_trend.js +14 -14
  171. package/module/musician_sign.js +7 -7
  172. package/module/musician_tasks.js +11 -11
  173. package/module/musician_tasks_new.js +11 -11
  174. package/module/musician_vip_tasks.js +11 -11
  175. package/module/mv_all.js +16 -16
  176. package/module/mv_detail.js +9 -9
  177. package/module/mv_detail_info.js +14 -14
  178. package/module/mv_exclusive_rcmd.js +10 -10
  179. package/module/mv_first.js +12 -12
  180. package/module/mv_sub.js +11 -11
  181. package/module/mv_sublist.js +15 -15
  182. package/module/mv_url.js +14 -14
  183. package/module/nickname_check.js +7 -7
  184. package/module/personal_fm.js +6 -6
  185. package/module/personal_fm_mode.js +14 -14
  186. package/module/personalized.js +16 -16
  187. package/module/personalized_djprogram.js +10 -10
  188. package/module/personalized_mv.js +6 -6
  189. package/module/personalized_newsong.js +15 -15
  190. package/module/personalized_privatecontent.js +10 -10
  191. package/module/personalized_privatecontent_list.js +15 -15
  192. package/module/pl_count.js +6 -6
  193. package/module/playlist_category_list.js +11 -11
  194. package/module/playlist_catlist.js +6 -6
  195. package/module/playlist_cover_update.js +32 -32
  196. package/module/playlist_create.js +11 -11
  197. package/module/playlist_delete.js +9 -9
  198. package/module/playlist_desc_update.js +10 -10
  199. package/module/playlist_detail.js +11 -11
  200. package/module/playlist_detail_dynamic.js +11 -11
  201. package/module/playlist_detail_rcmd_get.js +11 -11
  202. package/module/playlist_highquality_tags.js +10 -10
  203. package/module/playlist_hot.js +6 -6
  204. package/module/playlist_import_name_task_create.js +62 -62
  205. package/module/playlist_import_task_status.js +11 -11
  206. package/module/playlist_mylike.js +12 -12
  207. package/module/playlist_name_update.js +10 -10
  208. package/module/playlist_order_update.js +13 -13
  209. package/module/playlist_privacy.js +10 -10
  210. package/module/playlist_subscribe.js +14 -14
  211. package/module/playlist_subscribers.js +11 -11
  212. package/module/playlist_tags_update.js +10 -10
  213. package/module/playlist_track_add.js +16 -16
  214. package/module/playlist_track_all.js +31 -31
  215. package/module/playlist_track_delete.js +20 -20
  216. package/module/playlist_tracks.js +45 -45
  217. package/module/playlist_update.js +13 -13
  218. package/module/playlist_update_playcount.js +9 -9
  219. package/module/playlist_video_recent.js +9 -9
  220. package/module/playmode_intelligence_list.js +13 -13
  221. package/module/playmode_song_vector.js +8 -8
  222. package/module/program_recommend.js +15 -15
  223. package/module/radio_sport_get.js +9 -0
  224. package/module/rebind.js +16 -16
  225. package/module/recent_listen_list.js +7 -7
  226. package/module/recommend_resource.js +10 -10
  227. package/module/recommend_songs.js +13 -11
  228. package/module/recommend_songs_dislike.js +14 -14
  229. package/module/record_recent_album.js +11 -11
  230. package/module/record_recent_dj.js +11 -11
  231. package/module/record_recent_playlist.js +11 -11
  232. package/module/record_recent_song.js +11 -11
  233. package/module/record_recent_video.js +11 -11
  234. package/module/record_recent_voice.js +11 -11
  235. package/module/register_anonimous.js +53 -52
  236. package/module/register_cellphone.js +15 -15
  237. package/module/related_allvideo.js +14 -14
  238. package/module/related_playlist.js +32 -32
  239. package/module/resource_like.js +14 -14
  240. package/module/sati_resource_list.js +11 -0
  241. package/module/sati_resource_list_more.js +13 -0
  242. package/module/sati_resource_sub.js +10 -0
  243. package/module/sati_resource_sub_list.js +7 -0
  244. package/module/sati_tag_list.js +7 -0
  245. package/module/sati_timescene_resources_get.js +13 -0
  246. package/module/scrobble.js +26 -26
  247. package/module/search.js +21 -21
  248. package/module/search_default.js +6 -6
  249. package/module/search_hot.js +9 -9
  250. package/module/search_hot_detail.js +6 -6
  251. package/module/search_match.js +18 -18
  252. package/module/search_multimatch.js +14 -14
  253. package/module/search_suggest.js +14 -14
  254. package/module/search_suggest_pc.js +13 -13
  255. package/module/send_album.js +12 -12
  256. package/module/send_playlist.js +12 -12
  257. package/module/send_song.js +12 -12
  258. package/module/send_text.js +11 -11
  259. package/module/setting.js +7 -7
  260. package/module/share_resource.js +11 -11
  261. package/module/sheet_list.js +9 -9
  262. package/module/sheet_preview.js +8 -8
  263. package/module/sign_happy_info.js +5 -5
  264. package/module/signin_progress.js +13 -13
  265. package/module/simi_artist.js +12 -12
  266. package/module/simi_mv.js +9 -9
  267. package/module/simi_playlist.js +15 -15
  268. package/module/simi_song.js +15 -15
  269. package/module/simi_user.js +11 -11
  270. package/module/song_chorus.js +11 -11
  271. package/module/song_copyright_rcmd.js +9 -0
  272. package/module/song_creators.js +9 -0
  273. package/module/song_detail.js +11 -11
  274. package/module/song_downlist.js +11 -11
  275. package/module/song_download_url.js +10 -10
  276. package/module/song_download_url_v1.js +13 -13
  277. package/module/song_dynamic_cover.js +9 -9
  278. package/module/song_like.js +12 -12
  279. package/module/song_like_check.js +9 -9
  280. package/module/song_lyrics_mark.js +9 -9
  281. package/module/song_lyrics_mark_add.js +12 -12
  282. package/module/song_lyrics_mark_del.js +9 -9
  283. package/module/song_lyrics_mark_user_page.js +14 -14
  284. package/module/song_monthdownlist.js +11 -11
  285. package/module/song_music_detail.js +9 -9
  286. package/module/song_order_update.js +12 -12
  287. package/module/song_purchased.js +14 -14
  288. package/module/song_red_count.js +9 -9
  289. package/module/song_singledownlist.js +11 -11
  290. package/module/song_url.js +26 -26
  291. package/module/song_url_match.js +38 -38
  292. package/module/song_url_ncmget.js +5 -5
  293. package/module/song_url_v1.js +57 -57
  294. package/module/song_url_v1_302.js +53 -53
  295. package/module/song_wiki_summary.js +8 -8
  296. package/module/starpick_comments_summary.js +12 -12
  297. package/module/style_album.js +16 -16
  298. package/module/style_artist.js +16 -16
  299. package/module/style_detail.js +9 -9
  300. package/module/style_list.js +7 -7
  301. package/module/style_playlist.js +16 -16
  302. package/module/style_preference.js +11 -11
  303. package/module/style_song.js +12 -12
  304. package/module/summary_annual.js +12 -12
  305. package/module/threshold_detail_get.js +10 -10
  306. package/module/top_album.js +22 -22
  307. package/module/top_artists.js +11 -11
  308. package/module/top_list.js +20 -20
  309. package/module/top_mv.js +12 -12
  310. package/module/top_playlist.js +22 -22
  311. package/module/top_playlist_highquality.js +16 -16
  312. package/module/top_song.js +16 -16
  313. package/module/topic_detail.js +7 -7
  314. package/module/topic_detail_event_hot.js +7 -7
  315. package/module/topic_sublist.js +11 -11
  316. package/module/toplist.js +6 -6
  317. package/module/toplist_artist.js +12 -12
  318. package/module/toplist_detail.js +6 -6
  319. package/module/toplist_detail_v2.js +6 -6
  320. package/module/ugc_album_get.js +8 -8
  321. package/module/ugc_artist_get.js +8 -8
  322. package/module/ugc_artist_search.js +10 -10
  323. package/module/ugc_detail.js +17 -17
  324. package/module/ugc_mv_get.js +8 -8
  325. package/module/ugc_song_get.js +8 -8
  326. package/module/ugc_user_devote.js +6 -6
  327. package/module/user_account.js +5 -5
  328. package/module/user_audio.js +9 -9
  329. package/module/user_binding.js +9 -9
  330. package/module/user_bindingcellphone.js +15 -15
  331. package/module/user_cloud.js +10 -10
  332. package/module/user_cloud_del.js +9 -9
  333. package/module/user_cloud_detail.js +10 -10
  334. package/module/user_comment_history.js +15 -15
  335. package/module/user_detail.js +15 -15
  336. package/module/user_detail_new.js +20 -20
  337. package/module/user_dj.js +14 -14
  338. package/module/user_event.js +12 -12
  339. package/module/user_follow_mixed.js +23 -23
  340. package/module/user_followeds.js +17 -17
  341. package/module/user_follows.js +15 -15
  342. package/module/user_level.js +7 -7
  343. package/module/user_medal.js +11 -11
  344. package/module/user_mutualfollow_get.js +9 -9
  345. package/module/user_playlist.js +12 -12
  346. package/module/user_playlist_collect.js +14 -14
  347. package/module/user_playlist_create.js +14 -14
  348. package/module/user_record.js +10 -10
  349. package/module/user_replacephone.js +14 -14
  350. package/module/user_social_status.js +11 -11
  351. package/module/user_social_status_edit.js +16 -16
  352. package/module/user_social_status_rcmd.js +5 -5
  353. package/module/user_social_status_support.js +5 -5
  354. package/module/user_subcount.js +6 -6
  355. package/module/user_update.js +15 -15
  356. package/module/verify_getQr.js +39 -39
  357. package/module/verify_qrcodestatus.js +12 -12
  358. package/module/video_category_list.js +15 -15
  359. package/module/video_detail.js +13 -13
  360. package/module/video_detail_info.js +14 -14
  361. package/module/video_group.js +16 -16
  362. package/module/video_group_list.js +11 -11
  363. package/module/video_sub.js +14 -14
  364. package/module/video_timeline_all.js +17 -17
  365. package/module/video_timeline_recommend.js +13 -13
  366. package/module/video_url.js +10 -10
  367. package/module/vip_growthpoint.js +11 -11
  368. package/module/vip_growthpoint_details.js +14 -14
  369. package/module/vip_growthpoint_get.js +13 -13
  370. package/module/vip_info.js +12 -12
  371. package/module/vip_info_v2.js +12 -12
  372. package/module/vip_sign.js +7 -11
  373. package/module/vip_sign_info.js +11 -11
  374. package/module/vip_tasks.js +11 -11
  375. package/module/vip_timemachine.js +17 -17
  376. package/module/voice_delete.js +7 -7
  377. package/module/voice_detail.js +7 -7
  378. package/module/voice_lyric.js +7 -7
  379. package/module/voice_upload.js +200 -200
  380. package/module/voicelist_detail.js +11 -11
  381. package/module/voicelist_list.js +13 -13
  382. package/module/voicelist_list_search.js +14 -14
  383. package/module/voicelist_my_created.js +13 -13
  384. package/module/voicelist_search.js +11 -11
  385. package/module/voicelist_trans.js +15 -15
  386. package/module/weblog.js +10 -10
  387. package/module/yunbei.js +6 -6
  388. package/module/yunbei_expense.js +8 -8
  389. package/module/yunbei_info.js +5 -5
  390. package/module/yunbei_rcmd_song.js +17 -17
  391. package/module/yunbei_rcmd_song_history.js +16 -16
  392. package/module/yunbei_receipt.js +8 -8
  393. package/module/yunbei_sign.js +5 -5
  394. package/module/yunbei_task_finish.js +12 -12
  395. package/module/yunbei_tasks.js +9 -9
  396. package/module/yunbei_tasks_todo.js +9 -9
  397. package/module/yunbei_today.js +5 -5
  398. package/package.json +8 -8
  399. package/plugins/songUpload.js +109 -109
  400. package/plugins/upload.js +35 -35
  401. package/public/api.html +222 -193
  402. package/public/audio_match_demo/afp.js +1626 -1626
  403. package/public/audio_match_demo/afp.wasm.js +5 -5
  404. package/public/audio_match_demo/index.html +367 -367
  405. package/public/audio_match_demo/rec.js +49 -49
  406. package/public/avatar_update.html +326 -326
  407. package/public/cloud.html +578 -578
  408. package/public/docs/_coverpage.md +13 -13
  409. package/public/docs/home.md +5318 -5218
  410. package/public/docs/index.html +129 -47
  411. package/public/docs/logo.svg +6 -6
  412. package/public/docs/netease.png +0 -0
  413. package/public/docs/sw.js +90 -90
  414. package/public/eapi_decrypt.html +264 -226
  415. package/public/home.html +40 -40
  416. package/public/index.html +124 -124
  417. package/public/listen_together_host.html +552 -552
  418. package/public/login.html +219 -219
  419. package/public/playlist_cover_update.html +323 -323
  420. package/public/playlist_import.html +416 -416
  421. package/public/qrlogin-nocookie.html +199 -199
  422. package/public/qrlogin.html +199 -199
  423. package/public/static/2169.png +0 -0
  424. package/public/static/neteaselogo.png +0 -0
  425. package/public/unblock_test.html +153 -153
  426. package/public/voice_upload.html +326 -326
  427. package/server.js +454 -435
  428. package/util/apicache.js +836 -836
  429. package/util/client-sign.js +169 -169
  430. package/util/config.json +20 -20
  431. package/util/crypto.js +153 -135
  432. package/util/fileHelper.js +88 -88
  433. package/util/index.js +233 -233
  434. package/util/logger.js +42 -42
  435. package/util/memory-cache.js +71 -71
  436. package/util/option.js +14 -14
  437. package/util/request.js +367 -368
  438. package/module/comment_add.js +0 -15
  439. package/module/comment_reply.js +0 -13
package/util/apicache.js CHANGED
@@ -1,836 +1,836 @@
1
- var url = require('url')
2
- var MemoryCache = require('./memory-cache')
3
- const logger = require('./logger.js')
4
-
5
- var t = {
6
- ms: 1,
7
- second: 1000,
8
- minute: 60000,
9
- hour: 3600000,
10
- day: 3600000 * 24,
11
- week: 3600000 * 24 * 7,
12
- month: 3600000 * 24 * 30,
13
- }
14
-
15
- var instances = []
16
-
17
- var matches = function (a) {
18
- return function (b) {
19
- return a === b
20
- }
21
- }
22
-
23
- var doesntMatch = function (a) {
24
- return function (b) {
25
- return !matches(a)(b)
26
- }
27
- }
28
-
29
- var logDuration = function (d, prefix) {
30
- var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms'
31
- return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m'
32
- }
33
-
34
- function getSafeHeaders(res) {
35
- return res.getHeaders ? res.getHeaders() : res._headers
36
- }
37
-
38
- function ApiCache() {
39
- var memCache = new MemoryCache()
40
-
41
- var globalOptions = {
42
- debug: false,
43
- defaultDuration: 3600000,
44
- enabled: true,
45
- appendKey: [],
46
- jsonp: false,
47
- redisClient: false,
48
- headerBlacklist: [],
49
- statusCodes: {
50
- include: [],
51
- exclude: [],
52
- },
53
- events: {
54
- expire: undefined,
55
- },
56
- headers: {
57
- // 'cache-control': 'no-cache' // example of header overwrite
58
- },
59
- trackPerformance: false,
60
- }
61
-
62
- var middlewareOptions = []
63
- var instance = this
64
- var index = null
65
- var timers = {}
66
- var performanceArray = [] // for tracking cache hit rate
67
-
68
- instances.push(this)
69
- this.id = instances.length
70
-
71
- function debug(a, b, c, d) {
72
- var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) {
73
- return arg !== undefined
74
- })
75
- var debugEnv =
76
- process.env.DEBUG &&
77
- process.env.DEBUG.split(',').indexOf('apicache') !== -1
78
-
79
- return (globalOptions.debug || debugEnv) && console.log.apply(null, arr)
80
- }
81
-
82
- function shouldCacheResponse(request, response, toggle) {
83
- var opt = globalOptions
84
- var codes = opt.statusCodes
85
-
86
- if (!response) return false
87
-
88
- if (toggle && !toggle(request, response)) {
89
- return false
90
- }
91
-
92
- if (
93
- codes.exclude &&
94
- codes.exclude.length &&
95
- codes.exclude.indexOf(response.statusCode) !== -1
96
- )
97
- return false
98
- if (
99
- codes.include &&
100
- codes.include.length &&
101
- codes.include.indexOf(response.statusCode) === -1
102
- )
103
- return false
104
-
105
- return true
106
- }
107
-
108
- function addIndexEntries(key, req) {
109
- var groupName = req.apicacheGroup
110
-
111
- if (groupName) {
112
- debug('group detected "' + groupName + '"')
113
- var group = (index.groups[groupName] = index.groups[groupName] || [])
114
- group.unshift(key)
115
- }
116
-
117
- index.all.unshift(key)
118
- }
119
-
120
- function filterBlacklistedHeaders(headers) {
121
- return Object.keys(headers)
122
- .filter(function (key) {
123
- return globalOptions.headerBlacklist.indexOf(key) === -1
124
- })
125
- .reduce(function (acc, header) {
126
- acc[header] = headers[header]
127
- return acc
128
- }, {})
129
- }
130
-
131
- function createCacheObject(status, headers, data, encoding) {
132
- return {
133
- status: status,
134
- headers: filterBlacklistedHeaders(headers),
135
- data: data,
136
- encoding: encoding,
137
- timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
138
- }
139
- }
140
-
141
- function cacheResponse(key, value, duration) {
142
- var redis = globalOptions.redisClient
143
- var expireCallback = globalOptions.events.expire
144
-
145
- if (redis && redis.connected) {
146
- try {
147
- redis.hset(key, 'response', JSON.stringify(value))
148
- redis.hset(key, 'duration', duration)
149
- redis.expire(key, duration / 1000, expireCallback || function () {})
150
- } catch (err) {
151
- debug('[apicache] error in redis.hset()')
152
- }
153
- } else {
154
- memCache.add(key, value, duration, expireCallback)
155
- }
156
-
157
- // add automatic cache clearing from duration, includes max limit on setTimeout
158
- timers[key] = setTimeout(
159
- function () {
160
- instance.clear(key, true)
161
- },
162
- Math.min(duration, 2147483647),
163
- )
164
- }
165
-
166
- function accumulateContent(res, content) {
167
- if (content) {
168
- if (typeof content == 'string') {
169
- res._apicache.content = (res._apicache.content || '') + content
170
- } else if (Buffer.isBuffer(content)) {
171
- var oldContent = res._apicache.content
172
-
173
- if (typeof oldContent === 'string') {
174
- oldContent = !Buffer.from
175
- ? new Buffer(oldContent)
176
- : Buffer.from(oldContent)
177
- }
178
-
179
- if (!oldContent) {
180
- oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0)
181
- }
182
-
183
- res._apicache.content = Buffer.concat(
184
- [oldContent, content],
185
- oldContent.length + content.length,
186
- )
187
- } else {
188
- res._apicache.content = content
189
- }
190
- }
191
- }
192
-
193
- function makeResponseCacheable(
194
- req,
195
- res,
196
- next,
197
- key,
198
- duration,
199
- strDuration,
200
- toggle,
201
- ) {
202
- // monkeypatch res.end to create cache object
203
- res._apicache = {
204
- write: res.write,
205
- writeHead: res.writeHead,
206
- end: res.end,
207
- cacheable: true,
208
- content: undefined,
209
- }
210
-
211
- // append header overwrites if applicable
212
- Object.keys(globalOptions.headers).forEach(function (name) {
213
- res.setHeader(name, globalOptions.headers[name])
214
- })
215
-
216
- res.writeHead = function () {
217
- // add cache control headers
218
- if (!globalOptions.headers['cache-control']) {
219
- if (shouldCacheResponse(req, res, toggle)) {
220
- res.setHeader(
221
- 'cache-control',
222
- 'max-age=' + (duration / 1000).toFixed(0),
223
- )
224
- } else {
225
- res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
226
- }
227
- }
228
-
229
- res._apicache.headers = Object.assign({}, getSafeHeaders(res))
230
- return res._apicache.writeHead.apply(this, arguments)
231
- }
232
-
233
- // patch res.write
234
- res.write = function (content) {
235
- accumulateContent(res, content)
236
- return res._apicache.write.apply(this, arguments)
237
- }
238
-
239
- // patch res.end
240
- res.end = function (content, encoding) {
241
- if (shouldCacheResponse(req, res, toggle)) {
242
- accumulateContent(res, content)
243
-
244
- if (res._apicache.cacheable && res._apicache.content) {
245
- addIndexEntries(key, req)
246
- var headers = res._apicache.headers || getSafeHeaders(res)
247
- var cacheObject = createCacheObject(
248
- res.statusCode,
249
- headers,
250
- res._apicache.content,
251
- encoding,
252
- )
253
- cacheResponse(key, cacheObject, duration)
254
-
255
- // display log entry
256
- var elapsed = new Date() - req.apicacheTimer
257
- debug(
258
- 'adding cache entry for "' + key + '" @ ' + strDuration,
259
- logDuration(elapsed),
260
- )
261
- debug('_apicache.headers: ', res._apicache.headers)
262
- debug('res.getHeaders(): ', getSafeHeaders(res))
263
- debug('cacheObject: ', cacheObject)
264
- }
265
- }
266
-
267
- return res._apicache.end.apply(this, arguments)
268
- }
269
-
270
- next()
271
- }
272
-
273
- function sendCachedResponse(
274
- request,
275
- response,
276
- cacheObject,
277
- toggle,
278
- next,
279
- duration,
280
- ) {
281
- if (toggle && !toggle(request, response)) {
282
- return next()
283
- }
284
-
285
- var headers = getSafeHeaders(response)
286
-
287
- Object.assign(
288
- headers,
289
- filterBlacklistedHeaders(cacheObject.headers || {}),
290
- {
291
- // set properly-decremented max-age header. This ensures that max-age is in sync with the cache expiration.
292
- 'cache-control':
293
- 'max-age=' +
294
- Math.max(
295
- 0,
296
- (
297
- duration / 1000 -
298
- (new Date().getTime() / 1000 - cacheObject.timestamp)
299
- ).toFixed(0),
300
- ),
301
- },
302
- )
303
-
304
- // only embed apicache headers when not in production environment
305
-
306
- // unstringify buffers
307
- var data = cacheObject.data
308
- if (data && data.type === 'Buffer') {
309
- data =
310
- typeof data.data === 'number'
311
- ? new Buffer.alloc(data.data)
312
- : new Buffer.from(data.data)
313
- }
314
-
315
- // test Etag against If-None-Match for 304
316
- var cachedEtag = cacheObject.headers.etag
317
- var requestEtag = request.headers['if-none-match']
318
-
319
- if (requestEtag && cachedEtag === requestEtag) {
320
- response.writeHead(304, headers)
321
- return response.end()
322
- }
323
-
324
- response.writeHead(cacheObject.status || 200, headers)
325
-
326
- return response.end(data, cacheObject.encoding)
327
- }
328
-
329
- function syncOptions() {
330
- for (var i in middlewareOptions) {
331
- Object.assign(
332
- middlewareOptions[i].options,
333
- globalOptions,
334
- middlewareOptions[i].localOptions,
335
- )
336
- }
337
- }
338
-
339
- this.clear = function (target, isAutomatic) {
340
- var group = index.groups[target]
341
- var redis = globalOptions.redisClient
342
-
343
- if (group) {
344
- debug('clearing group "' + target + '"')
345
-
346
- group.forEach(function (key) {
347
- debug('clearing cached entry for "' + key + '"')
348
- clearTimeout(timers[key])
349
- delete timers[key]
350
- if (!globalOptions.redisClient) {
351
- memCache.delete(key)
352
- } else {
353
- try {
354
- redis.del(key)
355
- } catch (err) {
356
- logger.info('[apicache] error in redis.del("' + key + '")')
357
- }
358
- }
359
- index.all = index.all.filter(doesntMatch(key))
360
- })
361
-
362
- delete index.groups[target]
363
- } else if (target) {
364
- debug(
365
- 'clearing ' +
366
- (isAutomatic ? 'expired' : 'cached') +
367
- ' entry for "' +
368
- target +
369
- '"',
370
- )
371
- clearTimeout(timers[target])
372
- delete timers[target]
373
- // clear actual cached entry
374
- if (!redis) {
375
- memCache.delete(target)
376
- } else {
377
- try {
378
- redis.del(target)
379
- } catch (err) {
380
- logger.info('[apicache] error in redis.del("' + target + '")')
381
- }
382
- }
383
-
384
- // remove from global index
385
- index.all = index.all.filter(doesntMatch(target))
386
-
387
- // remove target from each group that it may exist in
388
- Object.keys(index.groups).forEach(function (groupName) {
389
- index.groups[groupName] = index.groups[groupName].filter(
390
- doesntMatch(target),
391
- )
392
-
393
- // delete group if now empty
394
- if (!index.groups[groupName].length) {
395
- delete index.groups[groupName]
396
- }
397
- })
398
- } else {
399
- debug('clearing entire index')
400
-
401
- if (!redis) {
402
- memCache.clear()
403
- } else {
404
- // clear redis keys one by one from internal index to prevent clearing non-apicache entries
405
- index.all.forEach(function (key) {
406
- clearTimeout(timers[key])
407
- delete timers[key]
408
- try {
409
- redis.del(key)
410
- } catch (err) {
411
- logger.info('[apicache] error in redis.del("' + key + '")')
412
- }
413
- })
414
- }
415
- this.resetIndex()
416
- }
417
-
418
- return this.getIndex()
419
- }
420
-
421
- function parseDuration(duration, defaultDuration) {
422
- if (typeof duration === 'number') return duration
423
-
424
- if (typeof duration === 'string') {
425
- var split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
426
-
427
- if (split.length === 3) {
428
- var len = parseFloat(split[1])
429
- var unit = split[2].replace(/s$/i, '').toLowerCase()
430
- if (unit === 'm') {
431
- unit = 'ms'
432
- }
433
-
434
- return (len || 1) * (t[unit] || 0)
435
- }
436
- }
437
-
438
- return defaultDuration
439
- }
440
-
441
- this.getDuration = function (duration) {
442
- return parseDuration(duration, globalOptions.defaultDuration)
443
- }
444
-
445
- /**
446
- * Return cache performance statistics (hit rate). Suitable for putting into a route:
447
- * <code>
448
- * app.get('/api/cache/performance', (req, res) => {
449
- * res.json(apicache.getPerformance())
450
- * })
451
- * </code>
452
- */
453
- this.getPerformance = function () {
454
- return performanceArray.map(function (p) {
455
- return p.report()
456
- })
457
- }
458
-
459
- this.getIndex = function (group) {
460
- if (group) {
461
- return index.groups[group]
462
- } else {
463
- return index
464
- }
465
- }
466
-
467
- this.middleware = function cache(
468
- strDuration,
469
- middlewareToggle,
470
- localOptions,
471
- ) {
472
- var duration = instance.getDuration(strDuration)
473
- var opt = {}
474
-
475
- middlewareOptions.push({
476
- options: opt,
477
- })
478
-
479
- var options = function (localOptions) {
480
- if (localOptions) {
481
- middlewareOptions.find(function (middleware) {
482
- return middleware.options === opt
483
- }).localOptions = localOptions
484
- }
485
-
486
- syncOptions()
487
-
488
- return opt
489
- }
490
-
491
- options(localOptions)
492
-
493
- /**
494
- * A Function for non tracking performance
495
- */
496
- function NOOPCachePerformance() {
497
- this.report = this.hit = this.miss = function () {} // noop;
498
- }
499
-
500
- /**
501
- * A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
502
- */
503
- function CachePerformance() {
504
- /**
505
- * Tracks the hit rate for the last 100 requests.
506
- * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
507
- */
508
- this.hitsLast100 = new Uint8Array(100 / 4) // each hit is 2 bits
509
-
510
- /**
511
- * Tracks the hit rate for the last 1000 requests.
512
- * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
513
- */
514
- this.hitsLast1000 = new Uint8Array(1000 / 4) // each hit is 2 bits
515
-
516
- /**
517
- * Tracks the hit rate for the last 10000 requests.
518
- * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
519
- */
520
- this.hitsLast10000 = new Uint8Array(10000 / 4) // each hit is 2 bits
521
-
522
- /**
523
- * Tracks the hit rate for the last 100000 requests.
524
- * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
525
- */
526
- this.hitsLast100000 = new Uint8Array(100000 / 4) // each hit is 2 bits
527
-
528
- /**
529
- * The number of calls that have passed through the middleware since the server started.
530
- */
531
- this.callCount = 0
532
-
533
- /**
534
- * The total number of hits since the server started
535
- */
536
- this.hitCount = 0
537
-
538
- /**
539
- * The key from the last cache hit. This is useful in identifying which route these statistics apply to.
540
- */
541
- this.lastCacheHit = null
542
-
543
- /**
544
- * The key from the last cache miss. This is useful in identifying which route these statistics apply to.
545
- */
546
- this.lastCacheMiss = null
547
-
548
- /**
549
- * Return performance statistics
550
- */
551
- this.report = function () {
552
- return {
553
- lastCacheHit: this.lastCacheHit,
554
- lastCacheMiss: this.lastCacheMiss,
555
- callCount: this.callCount,
556
- hitCount: this.hitCount,
557
- missCount: this.callCount - this.hitCount,
558
- hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
559
- hitRateLast100: this.hitRate(this.hitsLast100),
560
- hitRateLast1000: this.hitRate(this.hitsLast1000),
561
- hitRateLast10000: this.hitRate(this.hitsLast10000),
562
- hitRateLast100000: this.hitRate(this.hitsLast100000),
563
- }
564
- }
565
-
566
- /**
567
- * Computes a cache hit rate from an array of hits and misses.
568
- * @param {Uint8Array} array An array representing hits and misses.
569
- * @returns a number between 0 and 1, or null if the array has no hits or misses
570
- */
571
- this.hitRate = function (array) {
572
- var hits = 0
573
- var misses = 0
574
- for (var i = 0; i < array.length; i++) {
575
- var n8 = array[i]
576
- for (j = 0; j < 4; j++) {
577
- switch (n8 & 3) {
578
- case 1:
579
- hits++
580
- break
581
- case 2:
582
- misses++
583
- break
584
- }
585
- n8 >>= 2
586
- }
587
- }
588
- var total = hits + misses
589
- if (total == 0) return null
590
- return hits / total
591
- }
592
-
593
- /**
594
- * Record a hit or miss in the given array. It will be recorded at a position determined
595
- * by the current value of the callCount variable.
596
- * @param {Uint8Array} array An array representing hits and misses.
597
- * @param {boolean} hit true for a hit, false for a miss
598
- * Each element in the array is 8 bits, and encodes 4 hit/miss records.
599
- * Each hit or miss is encoded as to bits as follows:
600
- * 00 means no hit or miss has been recorded in these bits
601
- * 01 encodes a hit
602
- * 10 encodes a miss
603
- */
604
- this.recordHitInArray = function (array, hit) {
605
- var arrayIndex = ~~(this.callCount / 4) % array.length
606
- var bitOffset = (this.callCount % 4) * 2 // 2 bits per record, 4 records per uint8 array element
607
- var clearMask = ~(3 << bitOffset)
608
- var record = (hit ? 1 : 2) << bitOffset
609
- array[arrayIndex] = (array[arrayIndex] & clearMask) | record
610
- }
611
-
612
- /**
613
- * Records the hit or miss in the tracking arrays and increments the call count.
614
- * @param {boolean} hit true records a hit, false records a miss
615
- */
616
- this.recordHit = function (hit) {
617
- this.recordHitInArray(this.hitsLast100, hit)
618
- this.recordHitInArray(this.hitsLast1000, hit)
619
- this.recordHitInArray(this.hitsLast10000, hit)
620
- this.recordHitInArray(this.hitsLast100000, hit)
621
- if (hit) this.hitCount++
622
- this.callCount++
623
- }
624
-
625
- /**
626
- * Records a hit event, setting lastCacheMiss to the given key
627
- * @param {string} key The key that had the cache hit
628
- */
629
- this.hit = function (key) {
630
- this.recordHit(true)
631
- this.lastCacheHit = key
632
- }
633
-
634
- /**
635
- * Records a miss event, setting lastCacheMiss to the given key
636
- * @param {string} key The key that had the cache miss
637
- */
638
- this.miss = function (key) {
639
- this.recordHit(false)
640
- this.lastCacheMiss = key
641
- }
642
- }
643
-
644
- var perf = globalOptions.trackPerformance
645
- ? new CachePerformance()
646
- : new NOOPCachePerformance()
647
-
648
- performanceArray.push(perf)
649
-
650
- var cache = function (req, res, next) {
651
- function bypass() {
652
- debug('bypass detected, skipping cache.')
653
- return next()
654
- }
655
-
656
- // initial bypass chances
657
- if (!opt.enabled) return bypass()
658
- if (
659
- req.headers['x-apicache-bypass'] ||
660
- req.headers['x-apicache-force-fetch']
661
- )
662
- return bypass()
663
-
664
- // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
665
- // if (typeof middlewareToggle === 'function') {
666
- // if (!middlewareToggle(req, res)) return bypass()
667
- // } else if (middlewareToggle !== undefined && !middlewareToggle) {
668
- // return bypass()
669
- // }
670
-
671
- // embed timer
672
- req.apicacheTimer = new Date()
673
-
674
- // In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
675
- var key =
676
- req.hostname +
677
- (req.originalUrl || req.url) +
678
- JSON.stringify(req.cookies)
679
- // Remove querystring from key if jsonp option is enabled
680
- if (opt.jsonp) {
681
- key = url.parse(key).pathname
682
- }
683
-
684
- // add appendKey (either custom function or response path)
685
- if (typeof opt.appendKey === 'function') {
686
- key += '$$appendKey=' + opt.appendKey(req, res)
687
- } else if (opt.appendKey.length > 0) {
688
- var appendKey = req
689
-
690
- for (var i = 0; i < opt.appendKey.length; i++) {
691
- appendKey = appendKey[opt.appendKey[i]]
692
- }
693
- key += '$$appendKey=' + appendKey
694
- }
695
-
696
- // attempt cache hit
697
- var redis = opt.redisClient
698
- var cached = !redis ? memCache.getValue(key) : null
699
-
700
- // send if cache hit from memory-cache
701
- if (cached) {
702
- var elapsed = new Date() - req.apicacheTimer
703
- debug(
704
- 'sending cached (memory-cache) version of',
705
- key,
706
- logDuration(elapsed),
707
- )
708
-
709
- perf.hit(key)
710
- return sendCachedResponse(
711
- req,
712
- res,
713
- cached,
714
- middlewareToggle,
715
- next,
716
- duration,
717
- )
718
- }
719
-
720
- // send if cache hit from redis
721
- if (redis && redis.connected) {
722
- try {
723
- redis.hgetall(key, function (err, obj) {
724
- if (!err && obj && obj.response) {
725
- var elapsed = new Date() - req.apicacheTimer
726
- debug(
727
- 'sending cached (redis) version of',
728
- key,
729
- logDuration(elapsed),
730
- )
731
-
732
- perf.hit(key)
733
- return sendCachedResponse(
734
- req,
735
- res,
736
- JSON.parse(obj.response),
737
- middlewareToggle,
738
- next,
739
- duration,
740
- )
741
- } else {
742
- perf.miss(key)
743
- return makeResponseCacheable(
744
- req,
745
- res,
746
- next,
747
- key,
748
- duration,
749
- strDuration,
750
- middlewareToggle,
751
- )
752
- }
753
- })
754
- } catch (err) {
755
- // bypass redis on error
756
- perf.miss(key)
757
- return makeResponseCacheable(
758
- req,
759
- res,
760
- next,
761
- key,
762
- duration,
763
- strDuration,
764
- middlewareToggle,
765
- )
766
- }
767
- } else {
768
- perf.miss(key)
769
- return makeResponseCacheable(
770
- req,
771
- res,
772
- next,
773
- key,
774
- duration,
775
- strDuration,
776
- middlewareToggle,
777
- )
778
- }
779
- }
780
-
781
- cache.options = options
782
-
783
- return cache
784
- }
785
-
786
- this.options = function (options) {
787
- if (options) {
788
- Object.assign(globalOptions, options)
789
- syncOptions()
790
-
791
- if ('defaultDuration' in options) {
792
- // Convert the default duration to a number in milliseconds (if needed)
793
- globalOptions.defaultDuration = parseDuration(
794
- globalOptions.defaultDuration,
795
- 3600000,
796
- )
797
- }
798
-
799
- if (globalOptions.trackPerformance) {
800
- debug(
801
- 'WARNING: using trackPerformance flag can cause high memory usage!',
802
- )
803
- }
804
-
805
- return this
806
- } else {
807
- return globalOptions
808
- }
809
- }
810
-
811
- this.resetIndex = function () {
812
- index = {
813
- all: [],
814
- groups: {},
815
- }
816
- }
817
-
818
- this.newInstance = function (config) {
819
- var instance = new ApiCache()
820
-
821
- if (config) {
822
- instance.options(config)
823
- }
824
-
825
- return instance
826
- }
827
-
828
- this.clone = function () {
829
- return this.newInstance(this.options())
830
- }
831
-
832
- // initialize index
833
- this.resetIndex()
834
- }
835
-
836
- module.exports = new ApiCache()
1
+ var url = require('url')
2
+ var MemoryCache = require('./memory-cache')
3
+ const logger = require('./logger.js')
4
+
5
+ var t = {
6
+ ms: 1,
7
+ second: 1000,
8
+ minute: 60000,
9
+ hour: 3600000,
10
+ day: 3600000 * 24,
11
+ week: 3600000 * 24 * 7,
12
+ month: 3600000 * 24 * 30,
13
+ }
14
+
15
+ var instances = []
16
+
17
+ var matches = function (a) {
18
+ return function (b) {
19
+ return a === b
20
+ }
21
+ }
22
+
23
+ var doesntMatch = function (a) {
24
+ return function (b) {
25
+ return !matches(a)(b)
26
+ }
27
+ }
28
+
29
+ var logDuration = function (d, prefix) {
30
+ var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms'
31
+ return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m'
32
+ }
33
+
34
+ function getSafeHeaders(res) {
35
+ return res.getHeaders ? res.getHeaders() : res._headers
36
+ }
37
+
38
+ function ApiCache() {
39
+ var memCache = new MemoryCache()
40
+
41
+ var globalOptions = {
42
+ debug: false,
43
+ defaultDuration: 3600000,
44
+ enabled: true,
45
+ appendKey: [],
46
+ jsonp: false,
47
+ redisClient: false,
48
+ headerBlacklist: [],
49
+ statusCodes: {
50
+ include: [],
51
+ exclude: [],
52
+ },
53
+ events: {
54
+ expire: undefined,
55
+ },
56
+ headers: {
57
+ // 'cache-control': 'no-cache' // example of header overwrite
58
+ },
59
+ trackPerformance: false,
60
+ }
61
+
62
+ var middlewareOptions = []
63
+ var instance = this
64
+ var index = null
65
+ var timers = {}
66
+ var performanceArray = [] // for tracking cache hit rate
67
+
68
+ instances.push(this)
69
+ this.id = instances.length
70
+
71
+ function debug(a, b, c, d) {
72
+ var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) {
73
+ return arg !== undefined
74
+ })
75
+ var debugEnv =
76
+ process.env.DEBUG &&
77
+ process.env.DEBUG.split(',').indexOf('apicache') !== -1
78
+
79
+ return (globalOptions.debug || debugEnv) && console.log.apply(null, arr)
80
+ }
81
+
82
+ function shouldCacheResponse(request, response, toggle) {
83
+ var opt = globalOptions
84
+ var codes = opt.statusCodes
85
+
86
+ if (!response) return false
87
+
88
+ if (toggle && !toggle(request, response)) {
89
+ return false
90
+ }
91
+
92
+ if (
93
+ codes.exclude &&
94
+ codes.exclude.length &&
95
+ codes.exclude.indexOf(response.statusCode) !== -1
96
+ )
97
+ return false
98
+ if (
99
+ codes.include &&
100
+ codes.include.length &&
101
+ codes.include.indexOf(response.statusCode) === -1
102
+ )
103
+ return false
104
+
105
+ return true
106
+ }
107
+
108
+ function addIndexEntries(key, req) {
109
+ var groupName = req.apicacheGroup
110
+
111
+ if (groupName) {
112
+ debug('group detected "' + groupName + '"')
113
+ var group = (index.groups[groupName] = index.groups[groupName] || [])
114
+ group.unshift(key)
115
+ }
116
+
117
+ index.all.unshift(key)
118
+ }
119
+
120
+ function filterBlacklistedHeaders(headers) {
121
+ return Object.keys(headers)
122
+ .filter(function (key) {
123
+ return globalOptions.headerBlacklist.indexOf(key) === -1
124
+ })
125
+ .reduce(function (acc, header) {
126
+ acc[header] = headers[header]
127
+ return acc
128
+ }, {})
129
+ }
130
+
131
+ function createCacheObject(status, headers, data, encoding) {
132
+ return {
133
+ status: status,
134
+ headers: filterBlacklistedHeaders(headers),
135
+ data: data,
136
+ encoding: encoding,
137
+ timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
138
+ }
139
+ }
140
+
141
+ function cacheResponse(key, value, duration) {
142
+ var redis = globalOptions.redisClient
143
+ var expireCallback = globalOptions.events.expire
144
+
145
+ if (redis && redis.connected) {
146
+ try {
147
+ redis.hset(key, 'response', JSON.stringify(value))
148
+ redis.hset(key, 'duration', duration)
149
+ redis.expire(key, duration / 1000, expireCallback || function () {})
150
+ } catch (err) {
151
+ debug('[apicache] error in redis.hset()')
152
+ }
153
+ } else {
154
+ memCache.add(key, value, duration, expireCallback)
155
+ }
156
+
157
+ // add automatic cache clearing from duration, includes max limit on setTimeout
158
+ timers[key] = setTimeout(
159
+ function () {
160
+ instance.clear(key, true)
161
+ },
162
+ Math.min(duration, 2147483647),
163
+ )
164
+ }
165
+
166
+ function accumulateContent(res, content) {
167
+ if (content) {
168
+ if (typeof content == 'string') {
169
+ res._apicache.content = (res._apicache.content || '') + content
170
+ } else if (Buffer.isBuffer(content)) {
171
+ var oldContent = res._apicache.content
172
+
173
+ if (typeof oldContent === 'string') {
174
+ oldContent = !Buffer.from
175
+ ? new Buffer(oldContent)
176
+ : Buffer.from(oldContent)
177
+ }
178
+
179
+ if (!oldContent) {
180
+ oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0)
181
+ }
182
+
183
+ res._apicache.content = Buffer.concat(
184
+ [oldContent, content],
185
+ oldContent.length + content.length,
186
+ )
187
+ } else {
188
+ res._apicache.content = content
189
+ }
190
+ }
191
+ }
192
+
193
+ function makeResponseCacheable(
194
+ req,
195
+ res,
196
+ next,
197
+ key,
198
+ duration,
199
+ strDuration,
200
+ toggle,
201
+ ) {
202
+ // monkeypatch res.end to create cache object
203
+ res._apicache = {
204
+ write: res.write,
205
+ writeHead: res.writeHead,
206
+ end: res.end,
207
+ cacheable: true,
208
+ content: undefined,
209
+ }
210
+
211
+ // append header overwrites if applicable
212
+ Object.keys(globalOptions.headers).forEach(function (name) {
213
+ res.setHeader(name, globalOptions.headers[name])
214
+ })
215
+
216
+ res.writeHead = function () {
217
+ // add cache control headers
218
+ if (!globalOptions.headers['cache-control']) {
219
+ if (shouldCacheResponse(req, res, toggle)) {
220
+ res.setHeader(
221
+ 'cache-control',
222
+ 'max-age=' + (duration / 1000).toFixed(0),
223
+ )
224
+ } else {
225
+ res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
226
+ }
227
+ }
228
+
229
+ res._apicache.headers = Object.assign({}, getSafeHeaders(res))
230
+ return res._apicache.writeHead.apply(this, arguments)
231
+ }
232
+
233
+ // patch res.write
234
+ res.write = function (content) {
235
+ accumulateContent(res, content)
236
+ return res._apicache.write.apply(this, arguments)
237
+ }
238
+
239
+ // patch res.end
240
+ res.end = function (content, encoding) {
241
+ if (shouldCacheResponse(req, res, toggle)) {
242
+ accumulateContent(res, content)
243
+
244
+ if (res._apicache.cacheable && res._apicache.content) {
245
+ addIndexEntries(key, req)
246
+ var headers = res._apicache.headers || getSafeHeaders(res)
247
+ var cacheObject = createCacheObject(
248
+ res.statusCode,
249
+ headers,
250
+ res._apicache.content,
251
+ encoding,
252
+ )
253
+ cacheResponse(key, cacheObject, duration)
254
+
255
+ // display log entry
256
+ var elapsed = new Date() - req.apicacheTimer
257
+ debug(
258
+ 'adding cache entry for "' + key + '" @ ' + strDuration,
259
+ logDuration(elapsed),
260
+ )
261
+ debug('_apicache.headers: ', res._apicache.headers)
262
+ debug('res.getHeaders(): ', getSafeHeaders(res))
263
+ debug('cacheObject: ', cacheObject)
264
+ }
265
+ }
266
+
267
+ return res._apicache.end.apply(this, arguments)
268
+ }
269
+
270
+ next()
271
+ }
272
+
273
+ function sendCachedResponse(
274
+ request,
275
+ response,
276
+ cacheObject,
277
+ toggle,
278
+ next,
279
+ duration,
280
+ ) {
281
+ if (toggle && !toggle(request, response)) {
282
+ return next()
283
+ }
284
+
285
+ var headers = getSafeHeaders(response)
286
+
287
+ Object.assign(
288
+ headers,
289
+ filterBlacklistedHeaders(cacheObject.headers || {}),
290
+ {
291
+ // set properly-decremented max-age header. This ensures that max-age is in sync with the cache expiration.
292
+ 'cache-control':
293
+ 'max-age=' +
294
+ Math.max(
295
+ 0,
296
+ (
297
+ duration / 1000 -
298
+ (new Date().getTime() / 1000 - cacheObject.timestamp)
299
+ ).toFixed(0),
300
+ ),
301
+ },
302
+ )
303
+
304
+ // only embed apicache headers when not in production environment
305
+
306
+ // unstringify buffers
307
+ var data = cacheObject.data
308
+ if (data && data.type === 'Buffer') {
309
+ data =
310
+ typeof data.data === 'number'
311
+ ? new Buffer.alloc(data.data)
312
+ : new Buffer.from(data.data)
313
+ }
314
+
315
+ // test Etag against If-None-Match for 304
316
+ var cachedEtag = cacheObject.headers.etag
317
+ var requestEtag = request.headers['if-none-match']
318
+
319
+ if (requestEtag && cachedEtag === requestEtag) {
320
+ response.writeHead(304, headers)
321
+ return response.end()
322
+ }
323
+
324
+ response.writeHead(cacheObject.status || 200, headers)
325
+
326
+ return response.end(data, cacheObject.encoding)
327
+ }
328
+
329
+ function syncOptions() {
330
+ for (var i in middlewareOptions) {
331
+ Object.assign(
332
+ middlewareOptions[i].options,
333
+ globalOptions,
334
+ middlewareOptions[i].localOptions,
335
+ )
336
+ }
337
+ }
338
+
339
+ this.clear = function (target, isAutomatic) {
340
+ var group = index.groups[target]
341
+ var redis = globalOptions.redisClient
342
+
343
+ if (group) {
344
+ debug('clearing group "' + target + '"')
345
+
346
+ group.forEach(function (key) {
347
+ debug('clearing cached entry for "' + key + '"')
348
+ clearTimeout(timers[key])
349
+ delete timers[key]
350
+ if (!globalOptions.redisClient) {
351
+ memCache.delete(key)
352
+ } else {
353
+ try {
354
+ redis.del(key)
355
+ } catch (err) {
356
+ logger.info('[apicache] error in redis.del("' + key + '")')
357
+ }
358
+ }
359
+ index.all = index.all.filter(doesntMatch(key))
360
+ })
361
+
362
+ delete index.groups[target]
363
+ } else if (target) {
364
+ debug(
365
+ 'clearing ' +
366
+ (isAutomatic ? 'expired' : 'cached') +
367
+ ' entry for "' +
368
+ target +
369
+ '"',
370
+ )
371
+ clearTimeout(timers[target])
372
+ delete timers[target]
373
+ // clear actual cached entry
374
+ if (!redis) {
375
+ memCache.delete(target)
376
+ } else {
377
+ try {
378
+ redis.del(target)
379
+ } catch (err) {
380
+ logger.info('[apicache] error in redis.del("' + target + '")')
381
+ }
382
+ }
383
+
384
+ // remove from global index
385
+ index.all = index.all.filter(doesntMatch(target))
386
+
387
+ // remove target from each group that it may exist in
388
+ Object.keys(index.groups).forEach(function (groupName) {
389
+ index.groups[groupName] = index.groups[groupName].filter(
390
+ doesntMatch(target),
391
+ )
392
+
393
+ // delete group if now empty
394
+ if (!index.groups[groupName].length) {
395
+ delete index.groups[groupName]
396
+ }
397
+ })
398
+ } else {
399
+ debug('clearing entire index')
400
+
401
+ if (!redis) {
402
+ memCache.clear()
403
+ } else {
404
+ // clear redis keys one by one from internal index to prevent clearing non-apicache entries
405
+ index.all.forEach(function (key) {
406
+ clearTimeout(timers[key])
407
+ delete timers[key]
408
+ try {
409
+ redis.del(key)
410
+ } catch (err) {
411
+ logger.info('[apicache] error in redis.del("' + key + '")')
412
+ }
413
+ })
414
+ }
415
+ this.resetIndex()
416
+ }
417
+
418
+ return this.getIndex()
419
+ }
420
+
421
+ function parseDuration(duration, defaultDuration) {
422
+ if (typeof duration === 'number') return duration
423
+
424
+ if (typeof duration === 'string') {
425
+ var split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
426
+
427
+ if (split.length === 3) {
428
+ var len = parseFloat(split[1])
429
+ var unit = split[2].replace(/s$/i, '').toLowerCase()
430
+ if (unit === 'm') {
431
+ unit = 'ms'
432
+ }
433
+
434
+ return (len || 1) * (t[unit] || 0)
435
+ }
436
+ }
437
+
438
+ return defaultDuration
439
+ }
440
+
441
+ this.getDuration = function (duration) {
442
+ return parseDuration(duration, globalOptions.defaultDuration)
443
+ }
444
+
445
+ /**
446
+ * Return cache performance statistics (hit rate). Suitable for putting into a route:
447
+ * <code>
448
+ * app.get('/api/cache/performance', (req, res) => {
449
+ * res.json(apicache.getPerformance())
450
+ * })
451
+ * </code>
452
+ */
453
+ this.getPerformance = function () {
454
+ return performanceArray.map(function (p) {
455
+ return p.report()
456
+ })
457
+ }
458
+
459
+ this.getIndex = function (group) {
460
+ if (group) {
461
+ return index.groups[group]
462
+ } else {
463
+ return index
464
+ }
465
+ }
466
+
467
+ this.middleware = function cache(
468
+ strDuration,
469
+ middlewareToggle,
470
+ localOptions,
471
+ ) {
472
+ var duration = instance.getDuration(strDuration)
473
+ var opt = {}
474
+
475
+ middlewareOptions.push({
476
+ options: opt,
477
+ })
478
+
479
+ var options = function (localOptions) {
480
+ if (localOptions) {
481
+ middlewareOptions.find(function (middleware) {
482
+ return middleware.options === opt
483
+ }).localOptions = localOptions
484
+ }
485
+
486
+ syncOptions()
487
+
488
+ return opt
489
+ }
490
+
491
+ options(localOptions)
492
+
493
+ /**
494
+ * A Function for non tracking performance
495
+ */
496
+ function NOOPCachePerformance() {
497
+ this.report = this.hit = this.miss = function () {} // noop;
498
+ }
499
+
500
+ /**
501
+ * A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
502
+ */
503
+ function CachePerformance() {
504
+ /**
505
+ * Tracks the hit rate for the last 100 requests.
506
+ * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
507
+ */
508
+ this.hitsLast100 = new Uint8Array(100 / 4) // each hit is 2 bits
509
+
510
+ /**
511
+ * Tracks the hit rate for the last 1000 requests.
512
+ * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
513
+ */
514
+ this.hitsLast1000 = new Uint8Array(1000 / 4) // each hit is 2 bits
515
+
516
+ /**
517
+ * Tracks the hit rate for the last 10000 requests.
518
+ * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
519
+ */
520
+ this.hitsLast10000 = new Uint8Array(10000 / 4) // each hit is 2 bits
521
+
522
+ /**
523
+ * Tracks the hit rate for the last 100000 requests.
524
+ * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
525
+ */
526
+ this.hitsLast100000 = new Uint8Array(100000 / 4) // each hit is 2 bits
527
+
528
+ /**
529
+ * The number of calls that have passed through the middleware since the server started.
530
+ */
531
+ this.callCount = 0
532
+
533
+ /**
534
+ * The total number of hits since the server started
535
+ */
536
+ this.hitCount = 0
537
+
538
+ /**
539
+ * The key from the last cache hit. This is useful in identifying which route these statistics apply to.
540
+ */
541
+ this.lastCacheHit = null
542
+
543
+ /**
544
+ * The key from the last cache miss. This is useful in identifying which route these statistics apply to.
545
+ */
546
+ this.lastCacheMiss = null
547
+
548
+ /**
549
+ * Return performance statistics
550
+ */
551
+ this.report = function () {
552
+ return {
553
+ lastCacheHit: this.lastCacheHit,
554
+ lastCacheMiss: this.lastCacheMiss,
555
+ callCount: this.callCount,
556
+ hitCount: this.hitCount,
557
+ missCount: this.callCount - this.hitCount,
558
+ hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
559
+ hitRateLast100: this.hitRate(this.hitsLast100),
560
+ hitRateLast1000: this.hitRate(this.hitsLast1000),
561
+ hitRateLast10000: this.hitRate(this.hitsLast10000),
562
+ hitRateLast100000: this.hitRate(this.hitsLast100000),
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Computes a cache hit rate from an array of hits and misses.
568
+ * @param {Uint8Array} array An array representing hits and misses.
569
+ * @returns a number between 0 and 1, or null if the array has no hits or misses
570
+ */
571
+ this.hitRate = function (array) {
572
+ var hits = 0
573
+ var misses = 0
574
+ for (var i = 0; i < array.length; i++) {
575
+ var n8 = array[i]
576
+ for (j = 0; j < 4; j++) {
577
+ switch (n8 & 3) {
578
+ case 1:
579
+ hits++
580
+ break
581
+ case 2:
582
+ misses++
583
+ break
584
+ }
585
+ n8 >>= 2
586
+ }
587
+ }
588
+ var total = hits + misses
589
+ if (total == 0) return null
590
+ return hits / total
591
+ }
592
+
593
+ /**
594
+ * Record a hit or miss in the given array. It will be recorded at a position determined
595
+ * by the current value of the callCount variable.
596
+ * @param {Uint8Array} array An array representing hits and misses.
597
+ * @param {boolean} hit true for a hit, false for a miss
598
+ * Each element in the array is 8 bits, and encodes 4 hit/miss records.
599
+ * Each hit or miss is encoded as to bits as follows:
600
+ * 00 means no hit or miss has been recorded in these bits
601
+ * 01 encodes a hit
602
+ * 10 encodes a miss
603
+ */
604
+ this.recordHitInArray = function (array, hit) {
605
+ var arrayIndex = ~~(this.callCount / 4) % array.length
606
+ var bitOffset = (this.callCount % 4) * 2 // 2 bits per record, 4 records per uint8 array element
607
+ var clearMask = ~(3 << bitOffset)
608
+ var record = (hit ? 1 : 2) << bitOffset
609
+ array[arrayIndex] = (array[arrayIndex] & clearMask) | record
610
+ }
611
+
612
+ /**
613
+ * Records the hit or miss in the tracking arrays and increments the call count.
614
+ * @param {boolean} hit true records a hit, false records a miss
615
+ */
616
+ this.recordHit = function (hit) {
617
+ this.recordHitInArray(this.hitsLast100, hit)
618
+ this.recordHitInArray(this.hitsLast1000, hit)
619
+ this.recordHitInArray(this.hitsLast10000, hit)
620
+ this.recordHitInArray(this.hitsLast100000, hit)
621
+ if (hit) this.hitCount++
622
+ this.callCount++
623
+ }
624
+
625
+ /**
626
+ * Records a hit event, setting lastCacheMiss to the given key
627
+ * @param {string} key The key that had the cache hit
628
+ */
629
+ this.hit = function (key) {
630
+ this.recordHit(true)
631
+ this.lastCacheHit = key
632
+ }
633
+
634
+ /**
635
+ * Records a miss event, setting lastCacheMiss to the given key
636
+ * @param {string} key The key that had the cache miss
637
+ */
638
+ this.miss = function (key) {
639
+ this.recordHit(false)
640
+ this.lastCacheMiss = key
641
+ }
642
+ }
643
+
644
+ var perf = globalOptions.trackPerformance
645
+ ? new CachePerformance()
646
+ : new NOOPCachePerformance()
647
+
648
+ performanceArray.push(perf)
649
+
650
+ var cache = function (req, res, next) {
651
+ function bypass() {
652
+ debug('bypass detected, skipping cache.')
653
+ return next()
654
+ }
655
+
656
+ // initial bypass chances
657
+ if (!opt.enabled) return bypass()
658
+ if (
659
+ req.headers['x-apicache-bypass'] ||
660
+ req.headers['x-apicache-force-fetch']
661
+ )
662
+ return bypass()
663
+
664
+ // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
665
+ // if (typeof middlewareToggle === 'function') {
666
+ // if (!middlewareToggle(req, res)) return bypass()
667
+ // } else if (middlewareToggle !== undefined && !middlewareToggle) {
668
+ // return bypass()
669
+ // }
670
+
671
+ // embed timer
672
+ req.apicacheTimer = new Date()
673
+
674
+ // In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
675
+ var key =
676
+ req.hostname +
677
+ (req.originalUrl || req.url) +
678
+ JSON.stringify(req.cookies)
679
+ // Remove querystring from key if jsonp option is enabled
680
+ if (opt.jsonp) {
681
+ key = url.parse(key).pathname
682
+ }
683
+
684
+ // add appendKey (either custom function or response path)
685
+ if (typeof opt.appendKey === 'function') {
686
+ key += '$$appendKey=' + opt.appendKey(req, res)
687
+ } else if (opt.appendKey.length > 0) {
688
+ var appendKey = req
689
+
690
+ for (var i = 0; i < opt.appendKey.length; i++) {
691
+ appendKey = appendKey[opt.appendKey[i]]
692
+ }
693
+ key += '$$appendKey=' + appendKey
694
+ }
695
+
696
+ // attempt cache hit
697
+ var redis = opt.redisClient
698
+ var cached = !redis ? memCache.getValue(key) : null
699
+
700
+ // send if cache hit from memory-cache
701
+ if (cached) {
702
+ var elapsed = new Date() - req.apicacheTimer
703
+ debug(
704
+ 'sending cached (memory-cache) version of',
705
+ key,
706
+ logDuration(elapsed),
707
+ )
708
+
709
+ perf.hit(key)
710
+ return sendCachedResponse(
711
+ req,
712
+ res,
713
+ cached,
714
+ middlewareToggle,
715
+ next,
716
+ duration,
717
+ )
718
+ }
719
+
720
+ // send if cache hit from redis
721
+ if (redis && redis.connected) {
722
+ try {
723
+ redis.hgetall(key, function (err, obj) {
724
+ if (!err && obj && obj.response) {
725
+ var elapsed = new Date() - req.apicacheTimer
726
+ debug(
727
+ 'sending cached (redis) version of',
728
+ key,
729
+ logDuration(elapsed),
730
+ )
731
+
732
+ perf.hit(key)
733
+ return sendCachedResponse(
734
+ req,
735
+ res,
736
+ JSON.parse(obj.response),
737
+ middlewareToggle,
738
+ next,
739
+ duration,
740
+ )
741
+ } else {
742
+ perf.miss(key)
743
+ return makeResponseCacheable(
744
+ req,
745
+ res,
746
+ next,
747
+ key,
748
+ duration,
749
+ strDuration,
750
+ middlewareToggle,
751
+ )
752
+ }
753
+ })
754
+ } catch (err) {
755
+ // bypass redis on error
756
+ perf.miss(key)
757
+ return makeResponseCacheable(
758
+ req,
759
+ res,
760
+ next,
761
+ key,
762
+ duration,
763
+ strDuration,
764
+ middlewareToggle,
765
+ )
766
+ }
767
+ } else {
768
+ perf.miss(key)
769
+ return makeResponseCacheable(
770
+ req,
771
+ res,
772
+ next,
773
+ key,
774
+ duration,
775
+ strDuration,
776
+ middlewareToggle,
777
+ )
778
+ }
779
+ }
780
+
781
+ cache.options = options
782
+
783
+ return cache
784
+ }
785
+
786
+ this.options = function (options) {
787
+ if (options) {
788
+ Object.assign(globalOptions, options)
789
+ syncOptions()
790
+
791
+ if ('defaultDuration' in options) {
792
+ // Convert the default duration to a number in milliseconds (if needed)
793
+ globalOptions.defaultDuration = parseDuration(
794
+ globalOptions.defaultDuration,
795
+ 3600000,
796
+ )
797
+ }
798
+
799
+ if (globalOptions.trackPerformance) {
800
+ debug(
801
+ 'WARNING: using trackPerformance flag can cause high memory usage!',
802
+ )
803
+ }
804
+
805
+ return this
806
+ } else {
807
+ return globalOptions
808
+ }
809
+ }
810
+
811
+ this.resetIndex = function () {
812
+ index = {
813
+ all: [],
814
+ groups: {},
815
+ }
816
+ }
817
+
818
+ this.newInstance = function (config) {
819
+ var instance = new ApiCache()
820
+
821
+ if (config) {
822
+ instance.options(config)
823
+ }
824
+
825
+ return instance
826
+ }
827
+
828
+ this.clone = function () {
829
+ return this.newInstance(this.options())
830
+ }
831
+
832
+ // initialize index
833
+ this.resetIndex()
834
+ }
835
+
836
+ module.exports = new ApiCache()