imapflow 1.7.8 → 2.0.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.
Files changed (296) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +8 -2
  3. package/dist/cjs/charsets.d.ts +1 -0
  4. package/dist/cjs/charsets.js +294 -0
  5. package/dist/cjs/commands/append.d.ts +22 -0
  6. package/dist/cjs/commands/append.js +151 -0
  7. package/dist/cjs/commands/authenticate.d.ts +24 -0
  8. package/dist/cjs/commands/authenticate.js +223 -0
  9. package/dist/cjs/commands/capability.d.ts +8 -0
  10. package/dist/cjs/commands/capability.js +32 -0
  11. package/dist/cjs/commands/close.d.ts +8 -0
  12. package/dist/cjs/commands/close.js +39 -0
  13. package/dist/cjs/commands/compress.d.ts +8 -0
  14. package/dist/cjs/commands/compress.js +56 -0
  15. package/dist/cjs/commands/copy.d.ts +13 -0
  16. package/dist/cjs/commands/copy.js +44 -0
  17. package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
  18. package/dist/cjs/commands/copyuid-parser.js +32 -0
  19. package/dist/cjs/commands/create.d.ts +11 -0
  20. package/dist/cjs/commands/create.js +80 -0
  21. package/dist/cjs/commands/delete.d.ts +11 -0
  22. package/dist/cjs/commands/delete.js +40 -0
  23. package/dist/cjs/commands/enable.d.ts +9 -0
  24. package/dist/cjs/commands/enable.js +61 -0
  25. package/dist/cjs/commands/esearch-parser.d.ts +17 -0
  26. package/dist/cjs/commands/esearch-parser.js +91 -0
  27. package/dist/cjs/commands/expunge.d.ts +12 -0
  28. package/dist/cjs/commands/expunge.js +60 -0
  29. package/dist/cjs/commands/fetch.d.ts +30 -0
  30. package/dist/cjs/commands/fetch.js +241 -0
  31. package/dist/cjs/commands/id.d.ts +10 -0
  32. package/dist/cjs/commands/id.js +80 -0
  33. package/dist/cjs/commands/idle.d.ts +9 -0
  34. package/dist/cjs/commands/idle.js +347 -0
  35. package/dist/cjs/commands/list.d.ts +16 -0
  36. package/dist/cjs/commands/list.js +518 -0
  37. package/dist/cjs/commands/login.d.ts +11 -0
  38. package/dist/cjs/commands/login.js +42 -0
  39. package/dist/cjs/commands/logout.d.ts +8 -0
  40. package/dist/cjs/commands/logout.js +47 -0
  41. package/dist/cjs/commands/move.d.ts +13 -0
  42. package/dist/cjs/commands/move.js +57 -0
  43. package/dist/cjs/commands/namespace.d.ts +25 -0
  44. package/dist/cjs/commands/namespace.js +139 -0
  45. package/dist/cjs/commands/noop.d.ts +8 -0
  46. package/dist/cjs/commands/noop.js +22 -0
  47. package/dist/cjs/commands/quota.d.ts +10 -0
  48. package/dist/cjs/commands/quota.js +119 -0
  49. package/dist/cjs/commands/rename.d.ts +12 -0
  50. package/dist/cjs/commands/rename.js +48 -0
  51. package/dist/cjs/commands/search.d.ts +15 -0
  52. package/dist/cjs/commands/search.js +228 -0
  53. package/dist/cjs/commands/select.d.ts +25 -0
  54. package/dist/cjs/commands/select.js +250 -0
  55. package/dist/cjs/commands/starttls.d.ts +8 -0
  56. package/dist/cjs/commands/starttls.js +30 -0
  57. package/dist/cjs/commands/status-fields.d.ts +14 -0
  58. package/dist/cjs/commands/status-fields.js +61 -0
  59. package/dist/cjs/commands/status.d.ts +12 -0
  60. package/dist/cjs/commands/status.js +108 -0
  61. package/dist/cjs/commands/store.d.ts +19 -0
  62. package/dist/cjs/commands/store.js +93 -0
  63. package/dist/cjs/commands/subscribe.d.ts +9 -0
  64. package/dist/cjs/commands/subscribe.js +31 -0
  65. package/dist/cjs/commands/unsubscribe.d.ts +9 -0
  66. package/dist/cjs/commands/unsubscribe.js +31 -0
  67. package/dist/cjs/connection-deadline.d.ts +49 -0
  68. package/dist/cjs/connection-deadline.js +91 -0
  69. package/dist/cjs/errors.d.ts +83 -0
  70. package/dist/cjs/errors.js +13 -0
  71. package/dist/cjs/handler/imap-compiler.d.ts +24 -0
  72. package/dist/cjs/handler/imap-compiler.js +285 -0
  73. package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
  74. package/dist/cjs/handler/imap-formal-syntax.js +121 -0
  75. package/dist/cjs/handler/imap-handler.d.ts +9 -0
  76. package/dist/cjs/handler/imap-handler.js +10 -0
  77. package/dist/cjs/handler/imap-parser.d.ts +16 -0
  78. package/dist/cjs/handler/imap-parser.js +90 -0
  79. package/dist/cjs/handler/imap-stream.d.ts +181 -0
  80. package/dist/cjs/handler/imap-stream.js +446 -0
  81. package/dist/cjs/handler/limits.d.ts +25 -0
  82. package/dist/cjs/handler/limits.js +51 -0
  83. package/dist/cjs/handler/parser-instance.d.ts +68 -0
  84. package/dist/cjs/handler/parser-instance.js +223 -0
  85. package/dist/cjs/handler/token-parser.d.ts +91 -0
  86. package/dist/cjs/handler/token-parser.js +673 -0
  87. package/dist/cjs/handler/types.d.ts +91 -0
  88. package/dist/cjs/handler/types.js +4 -0
  89. package/dist/cjs/imap-commands.d.ts +16 -0
  90. package/dist/cjs/imap-commands.js +74 -0
  91. package/dist/cjs/imap-flow.d.ts +676 -0
  92. package/dist/cjs/imap-flow.js +3949 -0
  93. package/dist/cjs/jp-decoder.d.ts +12 -0
  94. package/dist/cjs/jp-decoder.js +79 -0
  95. package/dist/cjs/limited-passthrough.d.ts +25 -0
  96. package/dist/cjs/limited-passthrough.js +54 -0
  97. package/dist/cjs/logger.d.ts +3 -0
  98. package/dist/cjs/logger.js +11 -0
  99. package/dist/cjs/package-info.d.ts +3 -0
  100. package/dist/cjs/package-info.js +7 -0
  101. package/dist/cjs/package.json +3 -0
  102. package/dist/cjs/proxy-connection.d.ts +33 -0
  103. package/dist/cjs/proxy-connection.js +392 -0
  104. package/dist/cjs/search-compiler.d.ts +34 -0
  105. package/dist/cjs/search-compiler.js +476 -0
  106. package/dist/cjs/special-use.d.ts +22 -0
  107. package/dist/cjs/special-use.js +911 -0
  108. package/dist/cjs/tools.d.ts +427 -0
  109. package/dist/cjs/tools.js +1496 -0
  110. package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
  111. package/dist/cjs/types.js +5 -0
  112. package/dist/esm/charsets.d.ts +1 -0
  113. package/{lib → dist/esm}/charsets.js +1 -6
  114. package/dist/esm/commands/append.d.ts +22 -0
  115. package/{lib → dist/esm}/commands/append.js +22 -52
  116. package/dist/esm/commands/authenticate.d.ts +24 -0
  117. package/{lib → dist/esm}/commands/authenticate.js +62 -87
  118. package/dist/esm/commands/capability.d.ts +8 -0
  119. package/{lib → dist/esm}/commands/capability.js +6 -9
  120. package/dist/esm/commands/close.d.ts +8 -0
  121. package/{lib → dist/esm}/commands/close.js +6 -10
  122. package/dist/esm/commands/compress.d.ts +8 -0
  123. package/{lib → dist/esm}/commands/compress.js +7 -11
  124. package/dist/esm/commands/copy.d.ts +13 -0
  125. package/{lib → dist/esm}/commands/copy.js +12 -20
  126. package/dist/esm/commands/copyuid-parser.d.ts +11 -0
  127. package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
  128. package/dist/esm/commands/create.d.ts +11 -0
  129. package/{lib → dist/esm}/commands/create.js +13 -27
  130. package/dist/esm/commands/delete.d.ts +11 -0
  131. package/{lib → dist/esm}/commands/delete.js +9 -14
  132. package/dist/esm/commands/enable.d.ts +9 -0
  133. package/{lib → dist/esm}/commands/enable.js +23 -30
  134. package/dist/esm/commands/esearch-parser.d.ts +17 -0
  135. package/dist/esm/commands/esearch-parser.js +88 -0
  136. package/dist/esm/commands/expunge.d.ts +12 -0
  137. package/{lib → dist/esm}/commands/expunge.js +17 -22
  138. package/dist/esm/commands/fetch.d.ts +30 -0
  139. package/{lib → dist/esm}/commands/fetch.js +32 -64
  140. package/dist/esm/commands/id.d.ts +10 -0
  141. package/{lib → dist/esm}/commands/id.js +17 -23
  142. package/dist/esm/commands/idle.d.ts +9 -0
  143. package/{lib → dist/esm}/commands/idle.js +47 -81
  144. package/dist/esm/commands/list.d.ts +16 -0
  145. package/{lib → dist/esm}/commands/list.js +56 -121
  146. package/dist/esm/commands/login.d.ts +11 -0
  147. package/{lib → dist/esm}/commands/login.js +10 -15
  148. package/dist/esm/commands/logout.d.ts +8 -0
  149. package/{lib → dist/esm}/commands/logout.js +9 -11
  150. package/dist/esm/commands/move.d.ts +13 -0
  151. package/{lib → dist/esm}/commands/move.js +13 -21
  152. package/dist/esm/commands/namespace.d.ts +25 -0
  153. package/{lib → dist/esm}/commands/namespace.js +34 -44
  154. package/dist/esm/commands/noop.d.ts +8 -0
  155. package/{lib → dist/esm}/commands/noop.js +6 -7
  156. package/dist/esm/commands/quota.d.ts +10 -0
  157. package/{lib → dist/esm}/commands/quota.js +18 -36
  158. package/dist/esm/commands/rename.d.ts +12 -0
  159. package/{lib → dist/esm}/commands/rename.js +10 -15
  160. package/dist/esm/commands/search.d.ts +15 -0
  161. package/{lib → dist/esm}/commands/search.js +36 -135
  162. package/dist/esm/commands/select.d.ts +25 -0
  163. package/{lib → dist/esm}/commands/select.js +33 -64
  164. package/dist/esm/commands/starttls.d.ts +8 -0
  165. package/{lib → dist/esm}/commands/starttls.js +6 -8
  166. package/dist/esm/commands/status-fields.d.ts +14 -0
  167. package/{lib → dist/esm}/commands/status-fields.js +5 -16
  168. package/dist/esm/commands/status.d.ts +12 -0
  169. package/{lib → dist/esm}/commands/status.js +18 -29
  170. package/dist/esm/commands/store.d.ts +19 -0
  171. package/{lib → dist/esm}/commands/store.js +24 -37
  172. package/dist/esm/commands/subscribe.d.ts +9 -0
  173. package/{lib → dist/esm}/commands/subscribe.js +8 -12
  174. package/dist/esm/commands/unsubscribe.d.ts +9 -0
  175. package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
  176. package/dist/esm/connection-deadline.d.ts +49 -0
  177. package/{lib → dist/esm}/connection-deadline.js +14 -25
  178. package/dist/esm/errors.d.ts +83 -0
  179. package/dist/esm/errors.js +9 -0
  180. package/dist/esm/handler/imap-compiler.d.ts +24 -0
  181. package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
  182. package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
  183. package/dist/esm/handler/imap-formal-syntax.js +117 -0
  184. package/dist/esm/handler/imap-handler.d.ts +9 -0
  185. package/dist/esm/handler/imap-handler.js +9 -0
  186. package/dist/esm/handler/imap-parser.d.ts +16 -0
  187. package/{lib → dist/esm}/handler/imap-parser.js +31 -44
  188. package/dist/esm/handler/imap-stream.d.ts +181 -0
  189. package/{lib → dist/esm}/handler/imap-stream.js +29 -121
  190. package/dist/esm/handler/limits.d.ts +25 -0
  191. package/{lib → dist/esm}/handler/limits.js +13 -22
  192. package/dist/esm/handler/parser-instance.d.ts +68 -0
  193. package/{lib → dist/esm}/handler/parser-instance.js +19 -47
  194. package/dist/esm/handler/token-parser.d.ts +91 -0
  195. package/{lib → dist/esm}/handler/token-parser.js +71 -155
  196. package/dist/esm/handler/types.d.ts +91 -0
  197. package/dist/esm/handler/types.js +3 -0
  198. package/dist/esm/imap-commands.d.ts +16 -0
  199. package/dist/esm/imap-commands.js +67 -0
  200. package/dist/esm/imap-flow.d.ts +676 -0
  201. package/{lib → dist/esm}/imap-flow.js +761 -1789
  202. package/dist/esm/jp-decoder.d.ts +12 -0
  203. package/{lib → dist/esm}/jp-decoder.js +6 -21
  204. package/dist/esm/limited-passthrough.d.ts +25 -0
  205. package/{lib → dist/esm}/limited-passthrough.js +7 -20
  206. package/dist/esm/logger.d.ts +3 -0
  207. package/dist/esm/logger.js +4 -0
  208. package/dist/esm/package-info.d.ts +3 -0
  209. package/dist/esm/package-info.js +4 -0
  210. package/dist/esm/package.json +3 -0
  211. package/dist/esm/proxy-connection.d.ts +33 -0
  212. package/{lib → dist/esm}/proxy-connection.js +56 -127
  213. package/dist/esm/search-compiler.d.ts +34 -0
  214. package/{lib → dist/esm}/search-compiler.js +54 -110
  215. package/dist/esm/special-use.d.ts +22 -0
  216. package/dist/esm/special-use.js +907 -0
  217. package/dist/esm/tools.d.ts +427 -0
  218. package/dist/esm/tools.js +1446 -0
  219. package/dist/esm/types.d.ts +828 -0
  220. package/dist/esm/types.js +4 -0
  221. package/package.json +60 -20
  222. package/.gitattributes +0 -1
  223. package/.github/CODE_OF_CONDUCT.md +0 -76
  224. package/.github/FUNDING.yml +0 -4
  225. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  226. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  227. package/.github/contributing.md +0 -17
  228. package/.github/workflows/release.yaml +0 -36
  229. package/.github/workflows/stale.yml +0 -29
  230. package/.github/workflows/test.yml +0 -51
  231. package/.ncurc.js +0 -4
  232. package/.prettierignore +0 -4
  233. package/.prettierrc.js +0 -8
  234. package/.release-please-manifest.json +0 -3
  235. package/CLAUDE.md +0 -104
  236. package/Gruntfile.js +0 -23
  237. package/eslint.config.js +0 -45
  238. package/lib/handler/imap-formal-syntax.js +0 -189
  239. package/lib/handler/imap-handler.js +0 -17
  240. package/lib/imap-commands.js +0 -45
  241. package/lib/logger.js +0 -5
  242. package/lib/special-use.js +0 -923
  243. package/lib/tools.js +0 -1612
  244. package/release-please-config.json +0 -10
  245. package/test/authentication-test.js +0 -101
  246. package/test/auto-idle-test.js +0 -470
  247. package/test/bodystructure-test.js +0 -899
  248. package/test/charsets-test.js +0 -161
  249. package/test/commands-branches-test.js +0 -1095
  250. package/test/commands-integration-test.js +0 -11124
  251. package/test/commands-test.js +0 -73
  252. package/test/connection-edge-cases-test.js +0 -1828
  253. package/test/connection-test.js +0 -162
  254. package/test/copyuid-parser-test.js +0 -173
  255. package/test/fetch-generator-test.js +0 -218
  256. package/test/fixtures/fake-timers.js +0 -115
  257. package/test/fixtures/serialized-mimetorture.js +0 -2738
  258. package/test/fixtures/test-client.js +0 -101
  259. package/test/fixtures/test-tls.js +0 -8
  260. package/test/handler-branches-test.js +0 -310
  261. package/test/idle-polling-test.js +0 -518
  262. package/test/imap-compiler-test.js +0 -809
  263. package/test/imap-flow-compress-test.js +0 -166
  264. package/test/imap-flow-coverage-test.js +0 -612
  265. package/test/imap-flow-fetch-download-test.js +0 -909
  266. package/test/imap-flow-internals-test.js +0 -725
  267. package/test/imap-flow-methods-test.js +0 -889
  268. package/test/imap-flow-proxy-paths-test.js +0 -366
  269. package/test/imap-flow-secure-test.js +0 -573
  270. package/test/imap-flow-server-test.js +0 -1474
  271. package/test/imap-formal-syntax-test.js +0 -293
  272. package/test/imap-parser-test.js +0 -1474
  273. package/test/imap-stream-edge-cases-test.js +0 -666
  274. package/test/imap-stream-test.js +0 -177
  275. package/test/imapflow-test.js +0 -258
  276. package/test/integration/README.md +0 -52
  277. package/test/integration/dovecot-test.conf +0 -27
  278. package/test/integration/rev2-live-test.js +0 -431
  279. package/test/integration/run-rev2-tests.sh +0 -75
  280. package/test/integration-test.js +0 -83
  281. package/test/jp-decoder-test.js +0 -304
  282. package/test/limited-passthrough-test.js +0 -299
  283. package/test/memory-cleanup-test.js +0 -144
  284. package/test/memory-leak-test.js +0 -667
  285. package/test/parser-limits-test.js +0 -292
  286. package/test/proxy-connection-test.js +0 -738
  287. package/test/reliability-improvements-test.js +0 -548
  288. package/test/search-compiler-test.js +0 -1300
  289. package/test/search-test.js +0 -329
  290. package/test/special-use-test.js +0 -418
  291. package/test/starttls-injection-test.js +0 -181
  292. package/test/tag-correlation-test.js +0 -333
  293. package/test/timer-policy-test.js +0 -227
  294. package/test/token-parser-test.js +0 -456
  295. package/test/tools-test.js +0 -2013
  296. package/test/unhandled-rejection-test.js +0 -661
@@ -1,10 +0,0 @@
1
- {
2
- "packages": {
3
- ".": {
4
- "release-type": "node",
5
- "package-name": "imapflow",
6
- "pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]",
7
- "include-component-in-tag": false
8
- }
9
- }
10
- }
@@ -1,101 +0,0 @@
1
- 'use strict';
2
-
3
- const { ImapFlow } = require('../lib/imap-flow');
4
- const { AuthenticationFailure } = require('../lib/tools');
5
-
6
- module.exports['Authentication: Password auth configuration'] = test => {
7
- let client = new ImapFlow({
8
- host: 'imap.example.com',
9
- auth: {
10
- user: 'testuser',
11
- pass: 'testpass'
12
- }
13
- });
14
-
15
- test.equal(client.options.auth.user, 'testuser');
16
- test.equal(client.options.auth.pass, 'testpass');
17
- test.done();
18
- };
19
-
20
- module.exports['Authentication: OAuth2 auth configuration'] = test => {
21
- let client = new ImapFlow({
22
- host: 'imap.example.com',
23
- auth: {
24
- user: 'testuser',
25
- accessToken: 'oauth2_token_here'
26
- }
27
- });
28
-
29
- test.equal(client.options.auth.user, 'testuser');
30
- test.equal(client.options.auth.accessToken, 'oauth2_token_here');
31
- test.done();
32
- };
33
-
34
- module.exports['Authentication: Login method specification'] = test => {
35
- let client = new ImapFlow({
36
- host: 'imap.example.com',
37
- auth: {
38
- user: 'testuser',
39
- pass: 'testpass',
40
- loginMethod: 'AUTH=PLAIN'
41
- }
42
- });
43
-
44
- test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
45
- test.done();
46
- };
47
-
48
- module.exports['Authentication: SASL PLAIN with authzid for impersonation'] = test => {
49
- let client = new ImapFlow({
50
- host: 'imap.example.com',
51
- auth: {
52
- user: 'admin@example.com',
53
- pass: 'adminpass',
54
- authzid: 'user@example.com',
55
- loginMethod: 'AUTH=PLAIN'
56
- }
57
- });
58
-
59
- test.equal(client.options.auth.user, 'admin@example.com');
60
- test.equal(client.options.auth.pass, 'adminpass');
61
- test.equal(client.options.auth.authzid, 'user@example.com');
62
- test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
63
- test.done();
64
- };
65
-
66
- module.exports['Authentication: AuthenticationFailure error structure'] = test => {
67
- let error = new AuthenticationFailure('Invalid credentials');
68
-
69
- test.ok(error instanceof Error);
70
- test.equal(error.constructor.name, 'AuthenticationFailure');
71
- test.equal(error.message, 'Invalid credentials');
72
- test.done();
73
- };
74
-
75
- module.exports['Authentication: Verify-only mode'] = test => {
76
- let client = new ImapFlow({
77
- host: 'imap.example.com',
78
- auth: {
79
- user: 'testuser',
80
- pass: 'testpass'
81
- },
82
- verifyOnly: true
83
- });
84
-
85
- test.equal(client.options.verifyOnly, true);
86
- test.done();
87
- };
88
-
89
- module.exports['Authentication: Disable auto IDLE'] = test => {
90
- let client = new ImapFlow({
91
- host: 'imap.example.com',
92
- auth: {
93
- user: 'testuser',
94
- pass: 'testpass'
95
- },
96
- disableAutoIdle: true
97
- });
98
-
99
- test.equal(client.options.disableAutoIdle, true);
100
- test.done();
101
- };
@@ -1,470 +0,0 @@
1
- 'use strict';
2
-
3
- // Auto-IDLE policy: how the `autoIdleDelay` option is normalized, when the timer is allowed to
4
- // arm at all, and how the socket inactivity watchdog treats connections that are supposed to be
5
- // quiet.
6
- //
7
- // All of it matters more than the option looks. The configured value reaches setTimeout directly,
8
- // where NaN, a negative number and anything above the 32-bit range all become a 1ms timer, i.e.
9
- // an IDLE/DONE round trip around every single command. Arming while a caller still owns the
10
- // connection injects that round trip - or, with `missingIdleCommand` set to SELECT or STATUS, a
11
- // mailbox poll - between two of that caller's own commands. And the watchdog has to tell a
12
- // connection that is legitimately quiet (idling, a stalled download, a held lock) from one whose
13
- // command reply is overdue.
14
-
15
- const { makeClient, makeIdleReadyClient, chunkedFetchOne } = require('./fixtures/test-client');
16
- const { withFakeTimers } = require('./fixtures/fake-timers');
17
-
18
- const DEFAULT_DELAY = 15 * 1000;
19
- const DEFAULT_SOCKET_TIMEOUT = 5 * 60 * 1000;
20
- const SOCKET_MARGIN = 1000;
21
- const TIMEOUT_MAX = 2 ** 31 - 1;
22
-
23
- // Captures warn entries, so the "silently ignored configuration" cases can be asserted as
24
- // reported rather than guessed at.
25
- const makeLoggingClient = (overrides = {}) => {
26
- let warnings = [];
27
- let client = makeClient({
28
- ...overrides,
29
- logger: { trace() {}, debug() {}, info() {}, warn: entry => warnings.push(entry), error() {}, fatal() {} }
30
- });
31
- return { client, warnings };
32
- };
33
-
34
- // ============================================================================
35
- // option normalization
36
- // ============================================================================
37
-
38
- module.exports['Auto-IDLE: an unset delay uses the default'] = test => {
39
- let { client, warnings } = makeLoggingClient();
40
- test.equal(client.autoIdleDelay, DEFAULT_DELAY);
41
- test.equal(warnings.length, 0, 'the default is not a misconfiguration');
42
- test.done();
43
- };
44
-
45
- module.exports['Auto-IDLE: a configured delay is honored and reaches setTimeout'] = async test => {
46
- await withFakeTimers(async timers => {
47
- let client = makeIdleReadyClient({ autoIdleDelay: 1234 });
48
- test.equal(client.autoIdleDelay, 1234);
49
-
50
- client.autoidle();
51
-
52
- let armed = timers.pending();
53
- test.equal(armed.length, 1, 'exactly one auto-IDLE timer is armed');
54
- test.equal(armed[0].delay, 1234, 'the configured delay is what the timer uses');
55
-
56
- client.close();
57
- });
58
- test.done();
59
- };
60
-
61
- module.exports['Auto-IDLE: numeric strings are accepted'] = test => {
62
- // The normal shape of a value coming from an environment variable or a JSON/YAML config file
63
- test.equal(makeClient({ autoIdleDelay: '2000' }).autoIdleDelay, 2000);
64
- test.equal(makeClient({ autoIdleDelay: ' 2000 ' }).autoIdleDelay, 2000);
65
- test.done();
66
- };
67
-
68
- module.exports['Auto-IDLE: zero is honored, fractions are floored'] = test => {
69
- test.equal(makeClient({ autoIdleDelay: 0 }).autoIdleDelay, 0);
70
- test.equal(makeClient({ autoIdleDelay: 1500.9 }).autoIdleDelay, 1500);
71
- test.done();
72
- };
73
-
74
- module.exports['Auto-IDLE: unusable values fall back to the default and are reported'] = test => {
75
- // Every one of these is something setTimeout would turn into a 1ms timer, or something a
76
- // caller plausibly means as "off" - which is what disableAutoIdle is for.
77
- for (let value of [NaN, -1, -0.5, Infinity, -Infinity, 'soon', '', ' ', true, false, {}, []]) {
78
- let { client, warnings } = makeLoggingClient({ autoIdleDelay: value });
79
- test.equal(client.autoIdleDelay, DEFAULT_DELAY, `${String(value)} falls back to the default`);
80
- test.equal(warnings.length, 1, `${String(value)} is reported rather than silently swallowed`);
81
- test.equal(warnings[0].msg, 'Adjusted unusable autoIdleDelay option');
82
- test.equal(warnings[0].reason, 'not a non-negative finite number', `${String(value)} is reported with the right reason`);
83
- }
84
- test.done();
85
- };
86
-
87
- module.exports['Auto-IDLE: the delay is capped below socketTimeout'] = test => {
88
- // A delay at or above socketTimeout means the inactivity watchdog fires before IDLE ever
89
- // starts. `idling` is still false at that point, so the handler emits ETIMEOUT and tears down
90
- // a quiet but perfectly healthy connection instead of letting it enter IDLE.
91
- let { client, warnings } = makeLoggingClient({ autoIdleDelay: 10 * 60 * 1000 });
92
- test.equal(client.autoIdleDelay, DEFAULT_SOCKET_TIMEOUT - SOCKET_MARGIN);
93
- test.equal(warnings.length, 1, 'the caller is told the value was capped');
94
-
95
- // The cap follows a custom socketTimeout, and keeps the delay inside the range setTimeout can
96
- // represent: 2 ** 31 and larger would otherwise silently become a 1ms timer.
97
- test.equal(makeClient({ autoIdleDelay: 60000, socketTimeout: 30000 }).autoIdleDelay, 29000);
98
- test.equal(makeClient({ autoIdleDelay: 2 ** 31 }).autoIdleDelay, DEFAULT_SOCKET_TIMEOUT - SOCKET_MARGIN);
99
- test.equal(makeClient({ autoIdleDelay: Number.MAX_SAFE_INTEGER }).autoIdleDelay, DEFAULT_SOCKET_TIMEOUT - SOCKET_MARGIN);
100
- test.done();
101
- };
102
-
103
- module.exports['Auto-IDLE: the cap stays inside the timer range for a huge socketTimeout'] = test => {
104
- // socket.setTimeout truncates an over-range socketTimeout on its own, so a huge watchdog
105
- // deadline still works - but the auto-IDLE delay must not inherit the raw value, or
106
- // setTimeout would turn it into a 1ms timer and IDLE would follow every single command.
107
- test.equal(makeClient({ socketTimeout: 2 ** 32, autoIdleDelay: 2 ** 31 }).autoIdleDelay, TIMEOUT_MAX - SOCKET_MARGIN);
108
- test.equal(makeClient({ socketTimeout: 2200000000, autoIdleDelay: 2150000000 }).autoIdleDelay, TIMEOUT_MAX - SOCKET_MARGIN);
109
- test.done();
110
- };
111
-
112
- module.exports['Auto-IDLE: capping the default for a short socketTimeout is silent'] = test => {
113
- let { client, warnings } = makeLoggingClient({ socketTimeout: 5000 });
114
- test.equal(client.autoIdleDelay, 4000, 'the default is capped too');
115
- test.equal(warnings.length, 0, 'but nothing was misconfigured, so nothing is reported');
116
- test.done();
117
- };
118
-
119
- module.exports['Auto-IDLE: an invalid value keeps its own reason when the default is then capped'] = test => {
120
- // 'soon' falls back to the 15s default, which a 5s socketTimeout then caps to 4s. The
121
- // warning must still name what was wrong with the configured value - the cap applied to
122
- // the fallback, not to anything the caller asked for.
123
- let { client, warnings } = makeLoggingClient({ autoIdleDelay: 'soon', socketTimeout: 5000 });
124
- test.equal(client.autoIdleDelay, 4000);
125
- test.equal(warnings.length, 1);
126
- test.equal(warnings[0].reason, 'not a non-negative finite number');
127
- test.done();
128
- };
129
-
130
- // ============================================================================
131
- // when the timer may arm
132
- // ============================================================================
133
-
134
- module.exports['Auto-IDLE: no timer is armed while a mailbox lock is held'] = async test => {
135
- await withFakeTimers(async timers => {
136
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
137
-
138
- let lock = await client.getMailboxLock('INBOX');
139
- test.ok(client.currentLock, 'the lock was granted through the fast path');
140
-
141
- client.autoidle();
142
- test.equal(timers.count(), 0, 'IDLE must not be injected between a lock holder own commands');
143
-
144
- lock.release();
145
- let armed = timers.pending();
146
- test.equal(armed.length, 1, 'releasing the lock re-arms auto-IDLE');
147
- test.equal(armed[0].delay, 200);
148
-
149
- client.close();
150
- });
151
- test.done();
152
- };
153
-
154
- module.exports['Auto-IDLE: acquiring a lock through SELECT leaves no timer behind'] = async test => {
155
- await withFakeTimers(async timers => {
156
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
157
- client.mailbox = false;
158
- // The SELECT that opens the mailbox goes through run(), which re-arms auto-IDLE when it
159
- // settles - a moment before currentLock is set. That timer would fire inside the lock.
160
- client.mailboxOpen = async path => {
161
- client.mailbox = { path, readOnly: false };
162
- client.autoidle();
163
- return client.mailbox;
164
- };
165
-
166
- let lock = await client.getMailboxLock('INBOX');
167
- test.ok(client.currentLock, 'the lock was granted through the SELECT path');
168
- test.equal(timers.count(), 0, 'the timer armed while opening the mailbox was cleared');
169
-
170
- lock.release();
171
- test.equal(timers.count(), 1, 'and auto-IDLE resumes once the lock is released');
172
-
173
- client.close();
174
- });
175
- test.done();
176
- };
177
-
178
- module.exports['Auto-IDLE: releasing a lock with another one queued does not arm'] = async test => {
179
- await withFakeTimers(async timers => {
180
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
181
-
182
- let lock = await client.getMailboxLock('INBOX');
183
- // Queue a second request; the connection is not free when the first holder lets go
184
- let queued = client.getMailboxLock('INBOX');
185
- test.equal(client.locks.length, 1, 'the second request is waiting');
186
-
187
- lock.release();
188
- test.equal(timers.count(), 0, 'the next holder owns the connection, so IDLE stays off');
189
-
190
- (await queued).release();
191
- test.equal(timers.count(), 1, 'the last release re-arms auto-IDLE');
192
-
193
- client.close();
194
- });
195
- test.done();
196
- };
197
-
198
- module.exports['Auto-IDLE: no timer is armed while a command is in flight or queued'] = async test => {
199
- await withFakeTimers(async timers => {
200
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
201
-
202
- // Concurrent commands each clear the timer on entry, so without this guard the first one
203
- // to settle would arm a timer that fires while the others are still running.
204
- client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
205
- client.autoidle();
206
- test.equal(timers.count(), 0, 'a command in flight owns the connection');
207
-
208
- client.currentRequest = false;
209
- client.requestQueue = [{ tag: 'A002', command: 'FETCH' }];
210
- client.autoidle();
211
- test.equal(timers.count(), 0, 'so does a command still waiting in the queue');
212
-
213
- client.requestQueue = [];
214
- client.autoidle();
215
- test.equal(timers.count(), 1, 'an idle connection arms the timer');
216
-
217
- client.close();
218
- });
219
- test.done();
220
- };
221
-
222
- module.exports['Auto-IDLE: the armed timer re-checks the busy guard when it fires'] = async test => {
223
- await withFakeTimers(async timers => {
224
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
225
- let idleCalls = 0;
226
- client.idle = async () => {
227
- idleCalls++;
228
- };
229
-
230
- client.autoidle();
231
- test.equal(timers.count(), 1, 'the timer was armed on a free connection');
232
-
233
- // Ownership taken without clearing the timer - the guard must not depend on every
234
- // ownership-taking path remembering its clearTimeout.
235
- client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
236
- await timers.fire();
237
- test.equal(idleCalls, 0, 'a connection that is busy at fire time does not start IDLE');
238
-
239
- client.currentRequest = false;
240
- client.autoidle();
241
- await timers.fire();
242
- test.equal(idleCalls, 1, 'a connection that is free at fire time does');
243
-
244
- client.close();
245
- });
246
- test.done();
247
- };
248
-
249
- module.exports['Auto-IDLE: no timer is armed while a download is streaming'] = async test => {
250
- await withFakeTimers(async timers => {
251
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
252
- let body = Buffer.from('A'.repeat(20));
253
- let armedAfterChunk = [];
254
-
255
- let serveChunk = chunkedFetchOne(body);
256
- client.fetchOne = async (range, query) => {
257
- // Stands in for run(), which clears the auto-IDLE timer on entry and re-arms it once
258
- // the command settles. The pause between two chunks is where IDLE would slip in.
259
- clearTimeout(client.idleStartTimer);
260
- let part = await serveChunk(range, query);
261
- client.autoidle();
262
- armedAfterChunk.push(timers.count());
263
- return part;
264
- };
265
-
266
- let { content } = await client.download('1', false, { chunkSize: 4 });
267
- let received = [];
268
- for await (let chunk of content) {
269
- received.push(chunk);
270
- }
271
-
272
- test.equal(Buffer.concat(received).toString(), 'A'.repeat(20), 'the whole body arrived');
273
- test.ok(armedAfterChunk.length > 2, 'the body really was fetched in several chunks');
274
- test.deepEqual(
275
- armedAfterChunk.slice(1).filter(count => count !== 0),
276
- [],
277
- 'no chunk boundary inside the download armed an auto-IDLE timer'
278
- );
279
- test.equal(timers.count(), 1, 'auto-IDLE resumes once the download stream is finished');
280
-
281
- client.close();
282
- });
283
- test.done();
284
- };
285
-
286
- module.exports['Auto-IDLE: a download counts as busy before control returns to the event loop'] = async test => {
287
- await withFakeTimers(async () => {
288
- let client = makeIdleReadyClient({ autoIdleDelay: 200 });
289
- let body = Buffer.from('B'.repeat(12));
290
- client.fetchOne = chunkedFetchOne(body);
291
-
292
- let { content } = await client.download('1', false, { chunkSize: 4 });
293
- // The head chunk's own FETCH re-arms auto-IDLE a moment before the download exists;
294
- // with a very short delay that timer fires before the deferred chunk loop starts, so
295
- // the busy guard must already see the download when download() hands the stream back.
296
- test.equal(client._openDownloads, 1, 'the download is counted before streaming starts');
297
-
298
- let received = [];
299
- for await (let chunk of content) {
300
- received.push(chunk);
301
- }
302
- test.equal(Buffer.concat(received).toString(), 'B'.repeat(12), 'the whole body arrived');
303
- test.equal(client._openDownloads, 0, 'the download is released once the stream is done');
304
-
305
- client.close();
306
- });
307
- test.done();
308
- };
309
-
310
- module.exports['Auto-IDLE: a failing command still re-arms the timer'] = async test => {
311
- await withFakeTimers(async timers => {
312
- let client = makeIdleReadyClient({ autoIdleDelay: 300 });
313
- // run() clears the timer before dispatching, so re-arming only on the success path would
314
- // leave auto-IDLE off for good after one rejected command.
315
- client.runInternal = async () => {
316
- throw new Error('command failed');
317
- };
318
-
319
- let err;
320
- try {
321
- await client.run('NOOP');
322
- } catch (E) {
323
- err = E;
324
- }
325
-
326
- test.ok(err, 'the failure still reaches the caller');
327
- let armed = timers.pending();
328
- test.equal(armed.length, 1, 'auto-IDLE is re-armed after a failed command');
329
- test.equal(armed[0].delay, 300);
330
-
331
- client.close();
332
- });
333
- test.done();
334
- };
335
-
336
- module.exports['Auto-IDLE: a failing IDLE break still re-arms the timer'] = async test => {
337
- await withFakeTimers(async timers => {
338
- let client = makeIdleReadyClient({ autoIdleDelay: 300 });
339
- // run() awaits preCheck() (breaking an active IDLE) before dispatching. A break that
340
- // rejects exits run() before the command ever starts, and must still re-arm auto-IDLE
341
- // exactly like a failed command does.
342
- client.preCheck = async () => {
343
- throw new Error('IDLE break failed');
344
- };
345
-
346
- let err;
347
- try {
348
- await client.run('NOOP');
349
- } catch (E) {
350
- err = E;
351
- }
352
-
353
- test.ok(err, 'the failure still reaches the caller');
354
- let armed = timers.pending();
355
- test.equal(armed.length, 1, 'auto-IDLE is re-armed after the failed IDLE break');
356
- test.equal(armed[0].delay, 300);
357
-
358
- client.close();
359
- });
360
- test.done();
361
- };
362
-
363
- // ============================================================================
364
- // the socket inactivity watchdog
365
- // ============================================================================
366
-
367
- // A client wired up for _socketTimeout() with recording stubs: recovery commands land in
368
- // `recovered`, emitted errors in `errors`. run() is stubbed, so nothing here re-arms auto-IDLE -
369
- // which is deliberate: the handler itself must never start IDLE, that decision belongs to
370
- // autoidle() once the recovery NOOP settles for real.
371
- const makeWatchdogClient = () => {
372
- let client = makeIdleReadyClient();
373
- let errors = [];
374
- let recovered = [];
375
- client.on('error', err => errors.push(err));
376
- client.run = async command => recovered.push(command);
377
- client.idle = async () => recovered.push('IDLE');
378
- client.writeSocket = client.socket;
379
- client.setSocketHandlers();
380
- return { client, errors, recovered };
381
- };
382
-
383
- const drainImmediate = () => new Promise(resolve => setImmediate(resolve));
384
-
385
- module.exports['Auto-IDLE: a stalled download survives the socket watchdog'] = async test => {
386
- // Suppressing auto-IDLE during a download means `idling` is false when the inactivity
387
- // watchdog fires, so without the download clause the watchdog would tear down a connection
388
- // whose only problem is a consumer that stopped draining.
389
- let { client, errors, recovered } = makeWatchdogClient();
390
- client._openDownloads = 1;
391
-
392
- client._socketTimeout();
393
- await drainImmediate();
394
-
395
- test.deepEqual(errors, [], 'no ETIMEOUT is emitted while a download is open');
396
- test.deepEqual(recovered, ['NOOP'], 'kept alive with a NOOP only - IDLE mid-download is what the busy guard exists to prevent');
397
-
398
- client._openDownloads = 0;
399
- client._socketTimeout();
400
- test.equal(errors.length, 1, 'once the download is done a quiet socket is a timeout again');
401
- test.equal(errors[0].code, 'ETIMEOUT');
402
-
403
- client.close();
404
- test.ok(client.isClosed, 'the socket stub supports everything close() needs');
405
- test.done();
406
- };
407
-
408
- module.exports['Auto-IDLE: a stuck chunk FETCH mid-download is a dead connection'] = async test => {
409
- // A quiet socket while a command is awaiting its reply means the reply is overdue. A
410
- // recovery NOOP would only queue behind the stuck command and never reach the wire, so
411
- // the watchdog must report the timeout instead of recovering into a silent hang.
412
- let { client, errors, recovered } = makeWatchdogClient();
413
- client._openDownloads = 1;
414
- client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
415
-
416
- client._socketTimeout();
417
- await drainImmediate();
418
-
419
- test.deepEqual(recovered, [], 'no recovery is attempted behind a stuck command');
420
- test.equal(errors.length, 1, 'the caller learns the connection is dead');
421
- test.equal(errors[0].code, 'ETIMEOUT');
422
-
423
- client.close();
424
- test.done();
425
- };
426
-
427
- module.exports['Auto-IDLE: a held mailbox lock keeps a quiet connection alive'] = async test => {
428
- // A lock holder pausing between commands for longer than socketTimeout is legitimate (the
429
- // held-lock diagnostic warns only after 30 minutes), and with auto-IDLE declining to arm
430
- // during a lock there is no IDLE traffic to keep the socket busy - the watchdog has to.
431
- let { client, errors, recovered } = makeWatchdogClient();
432
- client.currentLock = { lockId: 1 };
433
-
434
- client._socketTimeout();
435
- await drainImmediate();
436
-
437
- test.deepEqual(errors, [], 'the lock holder keeps its connection');
438
- test.deepEqual(recovered, ['NOOP'], 'kept alive with a NOOP only, no IDLE inside the lock');
439
-
440
- client.currentLock = false;
441
- client._socketTimeout();
442
- test.equal(errors.length, 1, 'with the lock gone a quiet socket is a timeout again');
443
- test.equal(errors[0].code, 'ETIMEOUT');
444
-
445
- client.close();
446
- test.done();
447
- };
448
-
449
- module.exports['Auto-IDLE: idling recovers even with the IDLE command in flight'] = async test => {
450
- // During true IDLE the in-flight command IS the IDLE command; run() breaks it through
451
- // preCheck() before the recovery NOOP goes out, so it does not count as stuck. A recovery
452
- // NOOP that then never settles does: the next timeout must fail rather than queue another.
453
- let { client, errors, recovered } = makeWatchdogClient();
454
- client.idling = true;
455
- client.currentRequest = { tag: 'A001', command: 'IDLE', sent: true };
456
-
457
- client._socketTimeout();
458
- await drainImmediate();
459
- test.deepEqual(errors, [], 'the idling connection is not torn down');
460
- test.deepEqual(recovered, ['NOOP'], 'it is recovered with a NOOP');
461
-
462
- client.idling = false;
463
- client.currentRequest = { tag: 'A002', command: 'NOOP', sent: true };
464
- client._socketTimeout();
465
- test.equal(errors.length, 1, 'a recovery NOOP that never settled is a dead connection');
466
- test.equal(errors[0].code, 'ETIMEOUT');
467
-
468
- client.close();
469
- test.done();
470
- };