@tvlabs/wdio-service 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{dist → cjs}/channel.d.ts +4 -2
- package/cjs/channel.d.ts.map +1 -0
- package/cjs/channel.js +185 -0
- package/cjs/index.js +19 -0
- package/cjs/logger.d.ts +14 -0
- package/cjs/logger.d.ts.map +1 -0
- package/cjs/logger.js +67 -0
- package/cjs/package.json +1 -0
- package/{dist → cjs}/service.d.ts +2 -0
- package/cjs/service.d.ts.map +1 -0
- package/cjs/service.js +80 -0
- package/{dist → cjs}/types.d.ts +1 -0
- package/cjs/types.d.ts.map +1 -0
- package/cjs/types.js +2 -0
- package/esm/channel.d.ts +27 -0
- package/esm/channel.d.ts.map +1 -0
- package/{dist → esm}/channel.js +23 -19
- package/esm/index.d.ts +4 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/logger.d.ts +14 -0
- package/esm/logger.d.ts.map +1 -0
- package/esm/logger.js +63 -0
- package/esm/package.json +1 -0
- package/esm/service.d.ts +20 -0
- package/esm/service.d.ts.map +1 -0
- package/{dist → esm}/service.js +9 -4
- package/esm/types.d.ts +37 -0
- package/esm/types.d.ts.map +1 -0
- package/package.json +16 -15
- package/src/channel.ts +265 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +77 -0
- package/src/phoenix.d.ts +18 -0
- package/src/service.ts +128 -0
- package/src/types.ts +46 -0
- package/dist/channel.d.ts.map +0 -1
- package/dist/logger.d.ts +0 -3
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -2
- package/dist/service.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- /package/{dist → cjs}/index.d.ts +0 -0
- /package/{dist → cjs}/index.d.ts.map +0 -0
- /package/{dist → esm}/index.js +0 -0
- /package/{dist → esm}/types.js +0 -0
package/esm/logger.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const LOG_LEVEL_COLORS = {
|
|
3
|
+
error: chalk.red,
|
|
4
|
+
warn: chalk.yellow,
|
|
5
|
+
info: chalk.cyanBright,
|
|
6
|
+
debug: chalk.green,
|
|
7
|
+
trace: chalk.cyan,
|
|
8
|
+
silent: chalk.gray,
|
|
9
|
+
};
|
|
10
|
+
const LOG_LEVELS = {
|
|
11
|
+
error: 0,
|
|
12
|
+
warn: 1,
|
|
13
|
+
info: 2,
|
|
14
|
+
debug: 3,
|
|
15
|
+
trace: 4,
|
|
16
|
+
silent: 5,
|
|
17
|
+
};
|
|
18
|
+
export class Logger {
|
|
19
|
+
name;
|
|
20
|
+
logLevel;
|
|
21
|
+
constructor(name, logLevel = 'info') {
|
|
22
|
+
this.name = name;
|
|
23
|
+
this.logLevel = logLevel;
|
|
24
|
+
}
|
|
25
|
+
shouldLog(level) {
|
|
26
|
+
if (this.logLevel === 'silent') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
|
|
30
|
+
}
|
|
31
|
+
formatMessage(level, ...args) {
|
|
32
|
+
const timestamp = new Date().toISOString();
|
|
33
|
+
const levelColor = LOG_LEVEL_COLORS[level];
|
|
34
|
+
return `${chalk.gray(timestamp)} ${levelColor(level.toUpperCase())} ${chalk.white(this.name)}: ${args
|
|
35
|
+
.map((arg) => typeof arg === 'object' ? JSON.stringify(arg) : String(arg))
|
|
36
|
+
.join(' ')}`;
|
|
37
|
+
}
|
|
38
|
+
debug(...args) {
|
|
39
|
+
if (this.shouldLog('debug')) {
|
|
40
|
+
console.log(this.formatMessage('debug', ...args));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
info(...args) {
|
|
44
|
+
if (this.shouldLog('info')) {
|
|
45
|
+
console.log(this.formatMessage('info', ...args));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
warn(...args) {
|
|
49
|
+
if (this.shouldLog('warn')) {
|
|
50
|
+
console.warn(this.formatMessage('warn', ...args));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
error(...args) {
|
|
54
|
+
if (this.shouldLog('error')) {
|
|
55
|
+
console.error(this.formatMessage('error', ...args));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
trace(...args) {
|
|
59
|
+
if (this.shouldLog('trace')) {
|
|
60
|
+
console.trace(this.formatMessage('trace', ...args));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/esm/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "module"}
|
package/esm/service.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Services, Capabilities, Options } from '@wdio/types';
|
|
2
|
+
import type { TVLabsCapabilities, TVLabsServiceOptions } from './types.js';
|
|
3
|
+
export default class TVLabsService implements Services.ServiceInstance {
|
|
4
|
+
private _options;
|
|
5
|
+
private _capabilities;
|
|
6
|
+
private _config;
|
|
7
|
+
private log;
|
|
8
|
+
constructor(_options: TVLabsServiceOptions, _capabilities: Capabilities.ResolvedTestrunnerCapabilities, _config: Options.WebdriverIO);
|
|
9
|
+
onPrepare(_config: Options.Testrunner, param: Capabilities.TestrunnerCapabilities): void;
|
|
10
|
+
beforeSession(_config: Omit<Options.Testrunner, 'capabilities'>, capabilities: TVLabsCapabilities, _specs: string[], _cid: string): Promise<void>;
|
|
11
|
+
private setupRequestId;
|
|
12
|
+
private setRequestHeader;
|
|
13
|
+
private endpoint;
|
|
14
|
+
private retries;
|
|
15
|
+
private apiKey;
|
|
16
|
+
private logLevel;
|
|
17
|
+
private attachRequestId;
|
|
18
|
+
private reconnectRetries;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAIlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,GAAG,CAAS;gBAGV,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAQtC,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAStC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IAmBd,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
|
package/{dist → esm}/service.js
RENAMED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { SevereServiceError } from 'webdriverio';
|
|
2
|
-
import crypto from 'crypto';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { TVLabsChannel } from './channel.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Logger } from './logger.js';
|
|
6
6
|
export default class TVLabsService {
|
|
7
7
|
_options;
|
|
8
8
|
_capabilities;
|
|
9
9
|
_config;
|
|
10
|
+
log;
|
|
10
11
|
constructor(_options, _capabilities, _config) {
|
|
11
12
|
this._options = _options;
|
|
12
13
|
this._capabilities = _capabilities;
|
|
13
14
|
this._config = _config;
|
|
15
|
+
this.log = new Logger('@tvlabs/wdio-server', this._config.logLevel);
|
|
14
16
|
if (this.attachRequestId()) {
|
|
15
17
|
this.setupRequestId();
|
|
16
18
|
}
|
|
@@ -21,7 +23,7 @@ export default class TVLabsService {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
async beforeSession(_config, capabilities, _specs, _cid) {
|
|
24
|
-
const channel = new TVLabsChannel(this.endpoint(), this.reconnectRetries(), this.apiKey());
|
|
26
|
+
const channel = new TVLabsChannel(this.endpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
|
|
25
27
|
await channel.connect();
|
|
26
28
|
capabilities['tvlabs:session_id'] = await channel.newSession(capabilities, this.retries());
|
|
27
29
|
await channel.disconnect();
|
|
@@ -37,7 +39,7 @@ export default class TVLabsService {
|
|
|
37
39
|
originalRequestOptions.headers = {};
|
|
38
40
|
}
|
|
39
41
|
this.setRequestHeader(originalRequestOptions.headers, 'x-request-id', requestId);
|
|
40
|
-
log.info(chalk.blue('ATTACHED REQUEST ID'), requestId);
|
|
42
|
+
this.log.info(chalk.blue('ATTACHED REQUEST ID'), requestId);
|
|
41
43
|
return originalRequestOptions;
|
|
42
44
|
};
|
|
43
45
|
}
|
|
@@ -63,6 +65,9 @@ export default class TVLabsService {
|
|
|
63
65
|
apiKey() {
|
|
64
66
|
return this._options.apiKey;
|
|
65
67
|
}
|
|
68
|
+
logLevel() {
|
|
69
|
+
return this._config.logLevel ?? 'info';
|
|
70
|
+
}
|
|
66
71
|
attachRequestId() {
|
|
67
72
|
return this._options.attachRequestId ?? true;
|
|
68
73
|
}
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Capabilities } from '@wdio/types';
|
|
2
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
3
|
+
export type TVLabsServiceOptions = {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
retries?: number;
|
|
7
|
+
reconnectRetries?: number;
|
|
8
|
+
attachRequestId?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type TVLabsCapabilities = Capabilities.RequestedStandaloneCapabilities & {
|
|
11
|
+
'tvlabs:session_id'?: string;
|
|
12
|
+
'tvlabs:build'?: string;
|
|
13
|
+
'tvlabs:constraints'?: {
|
|
14
|
+
platform_key?: string;
|
|
15
|
+
device_type?: string;
|
|
16
|
+
make?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
year?: string;
|
|
19
|
+
minimum_chromedriver_major_version?: number;
|
|
20
|
+
supports_chromedriver?: boolean;
|
|
21
|
+
};
|
|
22
|
+
'tvlabs:match_timeout'?: number;
|
|
23
|
+
'tvlabs:device_timeout'?: number;
|
|
24
|
+
};
|
|
25
|
+
export type TVLabsSessionRequestEventHandler = (response: TVLabsSessionRequestUpdate) => void;
|
|
26
|
+
export type TVLabsSessionRequestUpdate = {
|
|
27
|
+
request_id: string;
|
|
28
|
+
session_id: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
export type TVLabsSessionRequestResponse = {
|
|
32
|
+
request_id: string;
|
|
33
|
+
};
|
|
34
|
+
export type TVLabsSessionChannelParams = {
|
|
35
|
+
api_key: string;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC5B,YAAY,CAAC,+BAA+B,GAAG;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kCAAkC,CAAC,EAAE,MAAM,CAAC;QAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEJ,MAAM,MAAM,gCAAgC,GAAG,CAC7C,QAAQ,EAAE,0BAA0B,KACjC,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tvlabs/wdio-service",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "WebdriverIO service that provides a better integration into TV Labs",
|
|
5
5
|
"author": "Regan Karlewicz <regan@tvlabs.ai>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,47 +22,48 @@
|
|
|
22
22
|
"url": "https://github.com/tv-labs/wdio-tvlabs-service/issues"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "
|
|
25
|
+
"build": "npm run build:cjs && npm run build:esm",
|
|
26
|
+
"build:cjs": "tsc -p src --module commonjs --moduleResolution node --outDir cjs && echo '{\"type\": \"commonjs\"}' > cjs/package.json",
|
|
27
|
+
"build:esm": "tsc -p src --outDir esm && echo '{\"type\": \"module\"}' > esm/package.json",
|
|
26
28
|
"start": "ts-node src/index.ts",
|
|
27
29
|
"dev": "ts-node --transpile-only src/index.ts",
|
|
28
|
-
"clean": "rm -rf
|
|
30
|
+
"clean": "rm -rf cjs && rm -rf esm",
|
|
29
31
|
"format": "prettier --write .",
|
|
30
32
|
"format:check": "prettier --check .",
|
|
31
33
|
"lint": "eslint",
|
|
32
|
-
"test": "vitest --config vitest.config.ts --coverage"
|
|
34
|
+
"test": "vitest --config vitest.config.ts --coverage",
|
|
35
|
+
"publish:dry": "npm publish --access public --provenance --dry-run"
|
|
33
36
|
},
|
|
34
37
|
"type": "module",
|
|
35
|
-
"types": "./
|
|
38
|
+
"types": "./esm/index.d.ts",
|
|
36
39
|
"exports": {
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"import": "./dist/index.js"
|
|
40
|
-
}
|
|
40
|
+
"require": "./cjs/index.js",
|
|
41
|
+
"import": "./esm/index.js"
|
|
41
42
|
},
|
|
43
|
+
"main": "cjs/index.js",
|
|
42
44
|
"typeScriptVersion": "5.8.2",
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@eslint/js": "^9.22.0",
|
|
45
47
|
"@types/node": "^22.13.10",
|
|
46
48
|
"@types/phoenix": "^1.6.6",
|
|
47
49
|
"@types/ws": "^8.18.0",
|
|
48
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
49
|
-
"@typescript-eslint/parser": "^8.
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^8.36.0",
|
|
51
|
+
"@typescript-eslint/parser": "^8.36.0",
|
|
50
52
|
"@vitest/coverage-v8": "^3.0.9",
|
|
51
53
|
"@wdio/globals": "^9.12.1",
|
|
52
|
-
"@wdio/logger": "^9.4.4",
|
|
53
54
|
"@wdio/types": "^9.10.1",
|
|
54
|
-
"eslint": "^9.
|
|
55
|
+
"eslint": "^9.30.1",
|
|
55
56
|
"globals": "^16.0.0",
|
|
56
57
|
"jiti": "^2.4.2",
|
|
57
58
|
"prettier": "^3.5.3",
|
|
58
59
|
"typescript": "^5.8.2",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
+
"typescript-eslint": "^8.36.0",
|
|
60
61
|
"vitest": "^3.0.9",
|
|
61
62
|
"webdriverio": "^9.12.1"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
65
|
"chalk": "^5.1.2",
|
|
65
66
|
"phoenix": "^1.7.20",
|
|
66
|
-
"ws": "^8.18.
|
|
67
|
+
"ws": "^8.18.3"
|
|
67
68
|
}
|
|
68
69
|
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { Socket, type Channel } from 'phoenix';
|
|
3
|
+
import { SevereServiceError } from 'webdriverio';
|
|
4
|
+
import { Logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
TVLabsCapabilities,
|
|
8
|
+
TVLabsSessionChannelParams,
|
|
9
|
+
TVLabsSessionRequestEventHandler,
|
|
10
|
+
TVLabsSessionRequestResponse,
|
|
11
|
+
LogLevel,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import type { PhoenixChannelJoinResponse } from './phoenix.js';
|
|
14
|
+
|
|
15
|
+
export class TVLabsChannel {
|
|
16
|
+
private socket: Socket;
|
|
17
|
+
private lobbyTopic: Channel;
|
|
18
|
+
private requestTopic?: Channel;
|
|
19
|
+
private log: Logger;
|
|
20
|
+
|
|
21
|
+
private readonly events = {
|
|
22
|
+
SESSION_READY: 'session:ready',
|
|
23
|
+
SESSION_FAILED: 'session:failed',
|
|
24
|
+
REQUEST_CANCELED: 'request:canceled',
|
|
25
|
+
REQUEST_FAILED: 'request:failed',
|
|
26
|
+
REQUEST_FILLED: 'request:filled',
|
|
27
|
+
REQUEST_MATCHING: 'request:matching',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private endpoint: string,
|
|
32
|
+
private maxReconnectRetries: number,
|
|
33
|
+
private key: string,
|
|
34
|
+
private logLevel: LogLevel,
|
|
35
|
+
) {
|
|
36
|
+
this.log = new Logger('@tvlabs/wdio-channel', this.logLevel);
|
|
37
|
+
this.socket = new Socket(this.endpoint, {
|
|
38
|
+
transport: WebSocket,
|
|
39
|
+
params: this.params(),
|
|
40
|
+
reconnectAfterMs: this.reconnectAfterMs.bind(this),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.socket.onError(this.logSocketError);
|
|
44
|
+
|
|
45
|
+
this.lobbyTopic = this.socket.channel('requests:lobby');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async disconnect(): Promise<void> {
|
|
49
|
+
return new Promise((res, _rej) => {
|
|
50
|
+
this.lobbyTopic.leave();
|
|
51
|
+
this.requestTopic?.leave();
|
|
52
|
+
this.socket.disconnect(() => res());
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async connect(): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
this.log.debug('Connecting to TV Labs...');
|
|
59
|
+
|
|
60
|
+
this.socket.connect();
|
|
61
|
+
|
|
62
|
+
await this.join(this.lobbyTopic);
|
|
63
|
+
|
|
64
|
+
this.log.debug('Connected to TV Labs!');
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.log.error('Error connecting to TV Labs:', error);
|
|
67
|
+
throw new SevereServiceError(
|
|
68
|
+
'Could not connect to TV Labs, please check your connection.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async newSession(
|
|
74
|
+
capabilities: TVLabsCapabilities,
|
|
75
|
+
maxRetries: number,
|
|
76
|
+
retry = 0,
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
try {
|
|
79
|
+
const requestId = await this.requestSession(capabilities);
|
|
80
|
+
const sessionId = await this.observeRequest(requestId);
|
|
81
|
+
|
|
82
|
+
return sessionId;
|
|
83
|
+
} catch {
|
|
84
|
+
return this.handleRetry(capabilities, maxRetries, retry);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async handleRetry(
|
|
89
|
+
capabilities: TVLabsCapabilities,
|
|
90
|
+
maxRetries: number,
|
|
91
|
+
retry: number,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
if (retry < maxRetries) {
|
|
94
|
+
this.log.warn(
|
|
95
|
+
`Could not create a session, retrying (${retry + 1}/${maxRetries})`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return this.newSession(capabilities, maxRetries, retry + 1);
|
|
99
|
+
} else {
|
|
100
|
+
throw new SevereServiceError(
|
|
101
|
+
`Could not create a session after ${maxRetries} attempts.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async observeRequest(requestId: string): Promise<string> {
|
|
107
|
+
const cleanup = () => this.unobserveRequest();
|
|
108
|
+
|
|
109
|
+
return new Promise<string>((res, rej) => {
|
|
110
|
+
this.requestTopic = this.socket.channel(`requests:${requestId}`);
|
|
111
|
+
|
|
112
|
+
const eventHandlers: Record<string, TVLabsSessionRequestEventHandler> = {
|
|
113
|
+
// Information events
|
|
114
|
+
[this.events.REQUEST_MATCHING]: ({ request_id }) => {
|
|
115
|
+
this.log.info(`Session request ${request_id} matching...`);
|
|
116
|
+
},
|
|
117
|
+
[this.events.REQUEST_FILLED]: ({ session_id, request_id }) => {
|
|
118
|
+
this.log.info(
|
|
119
|
+
`Session request ${request_id} filled: ${this.tvlabsSessionLink(session_id)}`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
this.log.info('Waiting for device to be ready...');
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Failure events
|
|
126
|
+
[this.events.SESSION_FAILED]: ({ session_id, reason }) => {
|
|
127
|
+
this.log.error(`Session ${session_id} failed, reason: ${reason}`);
|
|
128
|
+
rej(reason);
|
|
129
|
+
},
|
|
130
|
+
[this.events.REQUEST_CANCELED]: ({ request_id, reason }) => {
|
|
131
|
+
this.log.info(
|
|
132
|
+
`Session request ${request_id} canceled, reason: ${reason}`,
|
|
133
|
+
);
|
|
134
|
+
rej(reason);
|
|
135
|
+
},
|
|
136
|
+
[this.events.REQUEST_FAILED]: ({ request_id, reason }) => {
|
|
137
|
+
this.log.info(
|
|
138
|
+
`Session request ${request_id} failed, reason: ${reason}`,
|
|
139
|
+
);
|
|
140
|
+
rej(reason);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Ready event
|
|
144
|
+
[this.events.SESSION_READY]: ({ session_id }) => {
|
|
145
|
+
this.log.info(`Session ${session_id} ready!`);
|
|
146
|
+
res(session_id);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
Object.entries(eventHandlers).forEach(([event, handler]) => {
|
|
151
|
+
this.requestTopic?.on(event, handler);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.join(this.requestTopic).catch((err) => {
|
|
155
|
+
rej(err);
|
|
156
|
+
});
|
|
157
|
+
}).finally(cleanup);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private unobserveRequest() {
|
|
161
|
+
Object.values(this.events).forEach((event) => {
|
|
162
|
+
this.requestTopic?.off(event);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.requestTopic?.leave();
|
|
166
|
+
|
|
167
|
+
this.requestTopic = undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async requestSession(
|
|
171
|
+
capabilities: TVLabsCapabilities,
|
|
172
|
+
): Promise<string> {
|
|
173
|
+
this.log.info('Requesting TV Labs session');
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await this.push<TVLabsSessionRequestResponse>(
|
|
177
|
+
this.lobbyTopic,
|
|
178
|
+
'requests:create',
|
|
179
|
+
{ capabilities },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
this.log.info(
|
|
183
|
+
`Received session request ID: ${response.request_id}. Waiting for a match...`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return response.request_id;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
this.log.error('Error requesting session:', error);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async join(topic: Channel): Promise<void> {
|
|
194
|
+
return new Promise((res, rej) => {
|
|
195
|
+
topic
|
|
196
|
+
.join()
|
|
197
|
+
.receive('ok', (_resp: PhoenixChannelJoinResponse) => {
|
|
198
|
+
res();
|
|
199
|
+
})
|
|
200
|
+
.receive('error', ({ response }: PhoenixChannelJoinResponse) => {
|
|
201
|
+
rej('Failed to join topic: ' + response);
|
|
202
|
+
})
|
|
203
|
+
.receive('timeout', () => {
|
|
204
|
+
rej('timeout');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async push<T>(
|
|
210
|
+
topic: Channel,
|
|
211
|
+
event: string,
|
|
212
|
+
payload: object,
|
|
213
|
+
): Promise<T> {
|
|
214
|
+
return new Promise((res, rej) => {
|
|
215
|
+
topic
|
|
216
|
+
.push(event, payload)
|
|
217
|
+
.receive('ok', (msg: T) => {
|
|
218
|
+
res(msg);
|
|
219
|
+
})
|
|
220
|
+
.receive('error', (reason: string) => {
|
|
221
|
+
rej(reason);
|
|
222
|
+
})
|
|
223
|
+
.receive('timeout', () => {
|
|
224
|
+
rej('timeout');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private params(): TVLabsSessionChannelParams {
|
|
230
|
+
return {
|
|
231
|
+
api_key: this.key,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private reconnectAfterMs(tries: number) {
|
|
236
|
+
if (tries > this.maxReconnectRetries) {
|
|
237
|
+
throw new SevereServiceError(
|
|
238
|
+
'Could not connect to TV Labs, please check your connection.',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const wait = [0, 1000, 3000, 5000][tries] || 10000;
|
|
243
|
+
|
|
244
|
+
this.log.info(
|
|
245
|
+
`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return wait;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private logSocketError(
|
|
252
|
+
event: ErrorEvent,
|
|
253
|
+
_transport: new (endpoint: string) => object,
|
|
254
|
+
_establishedConnections: number,
|
|
255
|
+
) {
|
|
256
|
+
const error = event.error;
|
|
257
|
+
const code = error && error.code;
|
|
258
|
+
|
|
259
|
+
this.log.error('Socket error:', code || error || event);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private tvlabsSessionLink(sessionId: string) {
|
|
263
|
+
return `https://tvlabs.ai/app/sessions/${sessionId}`;
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { LogLevel } from './types.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
// TODO: Replace this with @wdio/logger
|
|
5
|
+
// It is currently not compatible with CJS
|
|
6
|
+
|
|
7
|
+
const LOG_LEVEL_COLORS: Record<LogLevel, typeof chalk> = {
|
|
8
|
+
error: chalk.red,
|
|
9
|
+
warn: chalk.yellow,
|
|
10
|
+
info: chalk.cyanBright,
|
|
11
|
+
debug: chalk.green,
|
|
12
|
+
trace: chalk.cyan,
|
|
13
|
+
silent: chalk.gray,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
17
|
+
error: 0,
|
|
18
|
+
warn: 1,
|
|
19
|
+
info: 2,
|
|
20
|
+
debug: 3,
|
|
21
|
+
trace: 4,
|
|
22
|
+
silent: 5,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class Logger {
|
|
26
|
+
constructor(
|
|
27
|
+
private name: string,
|
|
28
|
+
private logLevel: LogLevel = 'info',
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
private shouldLog(level: LogLevel): boolean {
|
|
32
|
+
if (this.logLevel === 'silent') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private formatMessage(level: LogLevel, ...args: unknown[]): string {
|
|
39
|
+
const timestamp = new Date().toISOString();
|
|
40
|
+
const levelColor = LOG_LEVEL_COLORS[level];
|
|
41
|
+
return `${chalk.gray(timestamp)} ${levelColor(level.toUpperCase())} ${chalk.white(this.name)}: ${args
|
|
42
|
+
.map((arg) =>
|
|
43
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
|
|
44
|
+
)
|
|
45
|
+
.join(' ')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
debug(...args: unknown[]): void {
|
|
49
|
+
if (this.shouldLog('debug')) {
|
|
50
|
+
console.log(this.formatMessage('debug', ...args));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
info(...args: unknown[]): void {
|
|
55
|
+
if (this.shouldLog('info')) {
|
|
56
|
+
console.log(this.formatMessage('info', ...args));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
warn(...args: unknown[]): void {
|
|
61
|
+
if (this.shouldLog('warn')) {
|
|
62
|
+
console.warn(this.formatMessage('warn', ...args));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
error(...args: unknown[]): void {
|
|
67
|
+
if (this.shouldLog('error')) {
|
|
68
|
+
console.error(this.formatMessage('error', ...args));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
trace(...args: unknown[]): void {
|
|
73
|
+
if (this.shouldLog('trace')) {
|
|
74
|
+
console.trace(this.formatMessage('trace', ...args));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/phoenix.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Socket as PhoenixSocket, MessageRef } from 'phoenix';
|
|
2
|
+
|
|
3
|
+
declare module 'phoenix' {
|
|
4
|
+
interface Socket extends PhoenixSocket {
|
|
5
|
+
onError(
|
|
6
|
+
callback: (
|
|
7
|
+
error: ErrorEvent,
|
|
8
|
+
transport: new (endpoint: string) => object,
|
|
9
|
+
establishedConnections: number,
|
|
10
|
+
) => void | Promise<void>,
|
|
11
|
+
): MessageRef;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PhoenixChannelJoinResponse = {
|
|
16
|
+
status?: string;
|
|
17
|
+
response?: unknown;
|
|
18
|
+
};
|