@browserless.io/browserless 2.9.0 → 2.10.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.
@@ -0,0 +1,76 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "cpu": {
5
+ "description": "An integer representing the percentage of CPU being used. For instance 92 means 92%",
6
+ "type": [
7
+ "null",
8
+ "number"
9
+ ]
10
+ },
11
+ "date": {
12
+ "description": "A number of milliseconds since epoch, or \"Date.now()\" equivalent.",
13
+ "type": "number"
14
+ },
15
+ "isAvailable": {
16
+ "description": "Whether or not a session can be connected and immediately ran on a health instance.",
17
+ "type": "boolean"
18
+ },
19
+ "maxConcurrent": {
20
+ "description": "The maximum amount of browsers that can be ran at a single time.",
21
+ "type": "number"
22
+ },
23
+ "maxQueued": {
24
+ "description": "The maximum amount of queued connections allowed at a single time.",
25
+ "type": "number"
26
+ },
27
+ "memory": {
28
+ "description": "An integer representing the percentage of Memory being used. For instance 95 means 95%",
29
+ "type": [
30
+ "null",
31
+ "number"
32
+ ]
33
+ },
34
+ "message": {
35
+ "description": "A human-readable message as the overall status of the instance.",
36
+ "type": "string"
37
+ },
38
+ "queued": {
39
+ "description": "The current number of connect or API calls pending to run.",
40
+ "type": "number"
41
+ },
42
+ "reason": {
43
+ "description": "A simple single-word reason as to why an instance may or may not be available.",
44
+ "enum": [
45
+ "",
46
+ "cpu",
47
+ "full",
48
+ "memory"
49
+ ],
50
+ "type": "string"
51
+ },
52
+ "recentlyRejected": {
53
+ "description": "The number of recent connections that were rejected due to the queue and concurrency\nlimits having been filled.",
54
+ "type": "number"
55
+ },
56
+ "running": {
57
+ "description": "The current number of running connections or API calls.",
58
+ "type": "number"
59
+ }
60
+ },
61
+ "additionalProperties": false,
62
+ "required": [
63
+ "cpu",
64
+ "date",
65
+ "isAvailable",
66
+ "maxConcurrent",
67
+ "maxQueued",
68
+ "memory",
69
+ "message",
70
+ "queued",
71
+ "reason",
72
+ "recentlyRejected",
73
+ "running"
74
+ ],
75
+ "$schema": "http://json-schema.org/draft-07/schema#"
76
+ }
@@ -31,6 +31,13 @@ describe('Management APIs', function () {
31
31
  expect(res.status).to.equal(200);
32
32
  });
33
33
  });
34
+ it('allows requests to /pressure', async () => {
35
+ await start();
36
+ await fetch('http://localhost:3000/pressure?token=6R0W53R135510').then(async (res) => {
37
+ expect(res.headers.get('content-type')).to.equal('application/json; charset=UTF-8');
38
+ expect(res.status).to.equal(200);
39
+ });
40
+ });
34
41
  it('allows requests to /sessions', async () => {
35
42
  await start();
36
43
  await fetch('http://localhost:3000/sessions?token=6R0W53R135510').then(async (res) => {
package/build/types.d.ts CHANGED
@@ -54,7 +54,8 @@ declare abstract class Route {
54
54
  protected _metrics: Browserless['metrics'];
55
55
  protected _monitoring: Browserless['monitoring'];
56
56
  protected _staticSDKDir: Browserless['staticSDKDir'];
57
- constructor(_browserManager: Browserless['browserManager'], _config: Browserless['config'], _fileSystem: Browserless['fileSystem'], _metrics: Browserless['metrics'], _monitoring: Browserless['monitoring'], _staticSDKDir: Browserless['staticSDKDir']);
57
+ protected _limiter: Browserless['limiter'];
58
+ constructor(_browserManager: Browserless['browserManager'], _config: Browserless['config'], _fileSystem: Browserless['fileSystem'], _metrics: Browserless['metrics'], _monitoring: Browserless['monitoring'], _staticSDKDir: Browserless['staticSDKDir'], _limiter: Browserless['limiter']);
58
59
  /**
59
60
  * A unique name to identify this route. Used in downstream
60
61
  * SDKs to potentially remove or disable.
@@ -133,6 +134,12 @@ declare abstract class Route {
133
134
  * @returns {string | null} The full path location of the SDK's static directory
134
135
  */
135
136
  staticSDKDir: () => string | null;
137
+ /**
138
+ * Helper function that loads the limiter module into the router's
139
+ * handler scope.
140
+ * @returns Limiter
141
+ */
142
+ limiter: () => import("@browserless.io/browserless").Limiter;
136
143
  /**
137
144
  * The HTTP path that this route handles, eg '/my-route' OR an
138
145
  * array of paths that this route can handle.
@@ -510,6 +517,7 @@ export declare const BrowserlessManagementRoutes: {
510
517
  ConfigGetRoute: string;
511
518
  MetricsGetRoute: string;
512
519
  MetricsTotalGetRoute: string;
520
+ PressureGetRoute: string;
513
521
  SessionsGetRoute: string;
514
522
  StaticGetRoute: string;
515
523
  };
@@ -518,6 +526,7 @@ export declare const BrowserlessRoutes: {
518
526
  ConfigGetRoute: string;
519
527
  MetricsGetRoute: string;
520
528
  MetricsTotalGetRoute: string;
529
+ PressureGetRoute: string;
521
530
  SessionsGetRoute: string;
522
531
  StaticGetRoute: string;
523
532
  WebKitPlaywrightWebSocketRoute: string;
package/build/types.js CHANGED
@@ -5,13 +5,15 @@ class Route {
5
5
  _metrics;
6
6
  _monitoring;
7
7
  _staticSDKDir;
8
- constructor(_browserManager, _config, _fileSystem, _metrics, _monitoring, _staticSDKDir) {
8
+ _limiter;
9
+ constructor(_browserManager, _config, _fileSystem, _metrics, _monitoring, _staticSDKDir, _limiter) {
9
10
  this._browserManager = _browserManager;
10
11
  this._config = _config;
11
12
  this._fileSystem = _fileSystem;
12
13
  this._metrics = _metrics;
13
14
  this._monitoring = _monitoring;
14
15
  this._staticSDKDir = _staticSDKDir;
16
+ this._limiter = _limiter;
15
17
  }
16
18
  /**
17
19
  * A boolean, or a function that returns a boolean, on
@@ -86,6 +88,12 @@ class Route {
86
88
  * @returns {string | null} The full path location of the SDK's static directory
87
89
  */
88
90
  staticSDKDir = () => this._staticSDKDir;
91
+ /**
92
+ * Helper function that loads the limiter module into the router's
93
+ * handler scope.
94
+ * @returns Limiter
95
+ */
96
+ limiter = () => this._limiter;
89
97
  }
90
98
  /**
91
99
  * A primitive HTTP-based route that doesn't require a
@@ -184,6 +192,7 @@ export const BrowserlessManagementRoutes = {
184
192
  ConfigGetRoute: 'ConfigGetRoute',
185
193
  MetricsGetRoute: 'MetricsGetRoute',
186
194
  MetricsTotalGetRoute: 'MetricsTotalGetRoute',
195
+ PressureGetRoute: 'PressureGetRoute',
187
196
  SessionsGetRoute: 'SessionsGetRoute',
188
197
  StaticGetRoute: 'StaticGetRoute',
189
198
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserless.io/browserless",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "license": "SSPL",
5
5
  "description": "The browserless platform",
6
6
  "author": "browserless.io",
@@ -59,11 +59,11 @@
59
59
  "lighthouse": "^12.0.0",
60
60
  "micromatch": "^4.0.4",
61
61
  "playwright-core": "1.44.0",
62
- "puppeteer-core": "^22.8.0",
62
+ "puppeteer-core": "^22.8.1",
63
63
  "puppeteer-extra": "^3.3.6",
64
64
  "puppeteer-extra-plugin-stealth": "^2.11.2",
65
65
  "queue": "^7.0.0",
66
- "systeminformation": "^5.22.8",
66
+ "systeminformation": "^5.22.9",
67
67
  "tar-fs": "^3.0.6"
68
68
  },
69
69
  "optionalDependencies": {
@@ -73,10 +73,10 @@
73
73
  "@types/http-proxy": "^1.17.14",
74
74
  "@types/micromatch": "^4.0.7",
75
75
  "@types/mocha": "^10.0.6",
76
- "@types/node": "^20.12.11",
76
+ "@types/node": "^20.12.12",
77
77
  "@types/sinon": "^17.0.3",
78
- "@typescript-eslint/eslint-plugin": "^7.8.0",
79
- "@typescript-eslint/parser": "^7.8.0",
78
+ "@typescript-eslint/eslint-plugin": "^7.9.0",
79
+ "@typescript-eslint/parser": "^7.9.0",
80
80
  "assert": "^2.0.0",
81
81
  "chai": "^5.1.1",
82
82
  "cross-env": "^7.0.3",
@@ -272,12 +272,14 @@ export class Browserless extends EventEmitter {
272
272
  this.metrics,
273
273
  this.monitoring,
274
274
  this.staticSDKDir,
275
+ this.limiter,
275
276
  );
276
277
 
277
278
  if (!this.routeIsDisabled(route)) {
278
279
  route.bodySchema = safeParse(bodySchema);
279
280
  route.querySchema = safeParse(querySchema);
280
281
  route.config = () => this.config;
282
+ route.limiter = () => this.limiter;
281
283
  route.metrics = () => this.metrics;
282
284
  route.monitoring = () => this.monitoring;
283
285
  route.fileSystem = () => this.fileSystem;
@@ -321,11 +323,13 @@ export class Browserless extends EventEmitter {
321
323
  this.metrics,
322
324
  this.monitoring,
323
325
  this.staticSDKDir,
326
+ this.limiter,
324
327
  );
325
328
 
326
329
  if (!this.routeIsDisabled(route)) {
327
330
  route.querySchema = safeParse(querySchema);
328
331
  route.config = () => this.config;
332
+ route.limiter = () => this.limiter;
329
333
  route.metrics = () => this.metrics;
330
334
  route.monitoring = () => this.monitoring;
331
335
  route.fileSystem = () => this.fileSystem;
package/src/http.ts CHANGED
@@ -128,6 +128,7 @@ export enum HTTPManagementRoutes {
128
128
  config = '/config',
129
129
  metrics = '/metrics',
130
130
  metricsTotal = '/metrics/total',
131
+ pressure = '/pressure',
131
132
  sessions = '/sessions',
132
133
  static = '/',
133
134
  }
@@ -0,0 +1,135 @@
1
+ import {
2
+ APITags,
3
+ BrowserlessRoutes,
4
+ HTTPManagementRoutes,
5
+ HTTPRoute,
6
+ Methods,
7
+ Request,
8
+ contentTypes,
9
+ jsonResponse,
10
+ } from '@browserless.io/browserless';
11
+ import { ServerResponse } from 'http';
12
+
13
+ export type ResponseSchema = {
14
+ /**
15
+ * An integer representing the percentage of CPU being used. For instance 92 means 92%
16
+ */
17
+ cpu: number | null;
18
+
19
+ /**
20
+ * A number of milliseconds since epoch, or "Date.now()" equivalent.
21
+ */
22
+ date: number;
23
+
24
+ /**
25
+ * Whether or not a session can be connected and immediately ran on a health instance.
26
+ */
27
+ isAvailable: boolean;
28
+
29
+ /**
30
+ * The maximum amount of browsers that can be ran at a single time.
31
+ */
32
+ maxConcurrent: number;
33
+
34
+ /**
35
+ * The maximum amount of queued connections allowed at a single time.
36
+ */
37
+ maxQueued: number;
38
+
39
+ /**
40
+ * An integer representing the percentage of Memory being used. For instance 95 means 95%
41
+ */
42
+ memory: number | null;
43
+
44
+ /**
45
+ * A human-readable message as the overall status of the instance.
46
+ */
47
+ message: string;
48
+
49
+ /**
50
+ * The current number of connect or API calls pending to run.
51
+ */
52
+ queued: number;
53
+
54
+ /**
55
+ * A simple single-word reason as to why an instance may or may not be available.
56
+ */
57
+ reason: 'full' | 'cpu' | 'memory' | '';
58
+
59
+ /**
60
+ * The number of recent connections that were rejected due to the queue and concurrency
61
+ * limits having been filled.
62
+ */
63
+ recentlyRejected: number;
64
+
65
+ /**
66
+ * The current number of running connections or API calls.
67
+ */
68
+ running: number;
69
+ };
70
+
71
+ export default class PressureGetRoute extends HTTPRoute {
72
+ name = BrowserlessRoutes.PressureGetRoute;
73
+ accepts = [contentTypes.any];
74
+ auth = true;
75
+ browser = null;
76
+ concurrency = false;
77
+ contentTypes = [contentTypes.json];
78
+ description = `Returns a JSON body of stats related to the pressure being created on the instance.`;
79
+ method = Methods.get;
80
+ path = HTTPManagementRoutes.pressure;
81
+ tags = [APITags.management];
82
+ handler = async (_req: Request, res: ServerResponse): Promise<void> => {
83
+ const monitoring = this.monitoring();
84
+ const config = this.config();
85
+ const limiter = this.limiter();
86
+ const metrics = this.metrics();
87
+
88
+ const {
89
+ cpuInt: cpu,
90
+ memoryInt: memory,
91
+ cpuOverloaded,
92
+ memoryOverloaded,
93
+ } = await monitoring.overloaded();
94
+ const date = Date.now();
95
+ const hasCapacity = limiter.hasCapacity;
96
+ const queued = limiter.waiting;
97
+ const isAvailable = hasCapacity && !cpuOverloaded && !memoryOverloaded;
98
+ const running = limiter.executing;
99
+ const recentlyRejected = metrics.get().rejected;
100
+ const maxConcurrent = config.getConcurrent();
101
+ const maxQueued = config.getQueued();
102
+
103
+ const reason = !hasCapacity
104
+ ? 'full'
105
+ : cpuOverloaded
106
+ ? 'cpu'
107
+ : memoryOverloaded
108
+ ? 'memory'
109
+ : '';
110
+
111
+ const message = !hasCapacity
112
+ ? 'Concurrency and queue are full'
113
+ : cpuOverloaded
114
+ ? 'CPU is over the configured maximum for cpu percent'
115
+ : memoryOverloaded
116
+ ? 'Memory is over the configured maximum for memory percent'
117
+ : '';
118
+
119
+ const response: ResponseSchema = {
120
+ cpu,
121
+ date,
122
+ isAvailable,
123
+ maxConcurrent,
124
+ maxQueued,
125
+ memory,
126
+ message,
127
+ queued,
128
+ reason,
129
+ recentlyRejected,
130
+ running,
131
+ };
132
+
133
+ return jsonResponse(res, 200, response);
134
+ };
135
+ }
@@ -56,6 +56,19 @@ describe('Management APIs', function () {
56
56
  );
57
57
  });
58
58
 
59
+ it('allows requests to /pressure', async () => {
60
+ await start();
61
+
62
+ await fetch('http://localhost:3000/pressure?token=6R0W53R135510').then(
63
+ async (res) => {
64
+ expect(res.headers.get('content-type')).to.equal(
65
+ 'application/json; charset=UTF-8',
66
+ );
67
+ expect(res.status).to.equal(200);
68
+ },
69
+ );
70
+ });
71
+
59
72
  it('allows requests to /sessions', async () => {
60
73
  await start();
61
74
 
@@ -87,13 +100,11 @@ describe('Management APIs', function () {
87
100
 
88
101
  await fetch('http://localhost:3000/active?token=6R0W53R135510', {
89
102
  method: 'HEAD',
90
- }).then(
91
- async (res) => {
92
- expect(res.headers.get('content-type')).to.equal(
93
- 'text/plain; charset=UTF-8',
94
- );
95
- expect(res.status).to.equal(204);
96
- },
97
- );
103
+ }).then(async (res) => {
104
+ expect(res.headers.get('content-type')).to.equal(
105
+ 'text/plain; charset=UTF-8',
106
+ );
107
+ expect(res.status).to.equal(204);
108
+ });
98
109
  });
99
110
  });
package/src/types.ts CHANGED
@@ -103,6 +103,7 @@ abstract class Route {
103
103
  protected _metrics: Browserless['metrics'],
104
104
  protected _monitoring: Browserless['monitoring'],
105
105
  protected _staticSDKDir: Browserless['staticSDKDir'],
106
+ protected _limiter: Browserless['limiter'],
106
107
  ) {}
107
108
 
108
109
  /**
@@ -196,6 +197,13 @@ abstract class Route {
196
197
  */
197
198
  staticSDKDir = () => this._staticSDKDir;
198
199
 
200
+ /**
201
+ * Helper function that loads the limiter module into the router's
202
+ * handler scope.
203
+ * @returns Limiter
204
+ */
205
+ limiter = () => this._limiter;
206
+
199
207
  /**
200
208
  * The HTTP path that this route handles, eg '/my-route' OR an
201
209
  * array of paths that this route can handle.
@@ -650,6 +658,7 @@ export const BrowserlessManagementRoutes = {
650
658
  ConfigGetRoute: 'ConfigGetRoute',
651
659
  MetricsGetRoute: 'MetricsGetRoute',
652
660
  MetricsTotalGetRoute: 'MetricsTotalGetRoute',
661
+ PressureGetRoute: 'PressureGetRoute',
653
662
  SessionsGetRoute: 'SessionsGetRoute',
654
663
  StaticGetRoute: 'StaticGetRoute',
655
664
  };