20220225のNode.jsに関する記事は6件です。

test

タイトル ssa ssa ssa ssa ssa ssa タイトル ssa ssa ssa ssa ssa ssa タイトル ssa ssa ssa ssa ssa ssa タイトル ssa ssa ssa ssa ssa ssa
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sqlite3でキーの配列に対応するレコードを取得する方法の実行時間比較

前提 Node.js で sqlite3 モジュールを使用して試していきます 色々見当違いなことをしているかもしれないので見つけた方はコメントで指摘していただけると嬉しいです やりたいこと 下記のようなテーブルが存在するとします id (PK) hash (UNIQUE) 1 abc 2 def 3 ghi この時、['def', 'def', 'ghi', 'abc'] という配列から [2, 2, 3, 1] という配列を得る良い方法を調べたのですが うまく見つけられませんでした そのため取得する方法を 4 種類考えて実際の実行時間を計測してみました ベスト (ベター) プラクティスをお持ちの方はコメントで教えてください (切実) 手順 レコード作成 ハッシュ値作成 とりあえず 1-100 の整数値の md5 ハッシュ値を作成します const length = 100 const crypto = require('crypto') const hashes = Array(length) .fill(0) .map((_, i) => crypto.createHash('md5').update(i.toString()).digest('hex')) テーブル作成 & インサート id (INTEGER) と hash (TEXT) をカラムに持つテーブルを作成し、 先ほど作成したハッシュ値をレコードとしてインサートします const sqlite3 = require('sqlite3') const db = new sqlite3.Database('./test.db') db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS hashes ( id INTEGER UNIQUE NOT NULL PRIMARY KEY, hash TEXT UNIQUE NOT NULL )`) db.run(`INSERT OR IGNORE INTO hashes ( hash ) VALUES ${Array(hashes.length).fill('(?)').join(',')}`, ...hashes ) }) 検索用キー配列作成 重複を許すハッシュ値の配列を作成します キーの個数は生成したレコードの 1/2 にします (適当) 1% の確率でテーブルに存在しないキーが生成されます const keys = Array(Math.ceil(length / 2)) .fill(0) .map(() => Math.ceil(Math.random() * length * 1.01)) .map(createHash) 検索関数作成 実際に keys から id の配列を得るための関数を作ります 前提として db.get と db.all を async 化した dbGet と dbAll 関数を作成しています コード async function dbGet(db, ...args) { return new Promise((resolve) => db.get(...args, (err, result) => resolve(result)) ) } async function dbAll(db, ...args) { return new Promise((resolve) => db.all(...args, (err, result) => resolve(result)) ) } 1. 愚直に一つずつ SELECT 何のひねりもなく、SELECT id FROM hashes WHERE hash = ? を N 件回します async function f1(keys) { return await Promise.all( keys.map((key) => dbGet(db, 'SELECT id FROM hashes WHERE hash = ?', key).then( (row) => row?.id ) ) ) } 2. WHERE IN を使う SELECT * FROM hashes WHERE hash IN (?, ?, ?, ..., ?) を実行します 結果は keys の順番と一致しないため、js 側で加工してやる必要があります async function f2(keys) { const rows = await dbAll( db, `SELECT * FROM hashes WHERE hash IN (${Array(keys.length) .fill('?') .join(',')})`, ...keys ) const dict = {} for (const { id, hash } of rows) { dict[hash] = id } return keys.map((key) => dict[key]) } 3. SELECT で一時的なテーブルを作成して LEFT OUTER JOIN する SELECT hashes.id FROM (SELECT ? AS hash UNION ALL SELECT ? UNION ALL ... SELECT ?) AS temp LEFT OUTER JOIN hashes ON temp.hash = hashes.hash します。長いですね SELECT ? で一行を作成し、すべてを UNION ALL で結合して一つのダミーテーブルを用意してから LEFT OUTER JOIN で id を引っ張ってきます async function f3(keys) { return ( await dbAll( db, `SELECT hashes.id FROM (SELECT ? AS hash ${Array(keys.length - 1) .fill('UNION ALL SELECT ?') .join('')} ) AS temp LEFT OUTER JOIN hashes ON temp.hash = hashes.hash`, ...keys ) )?.map?.((row) => row.id) } 4. テーブルを作って検索して消す f3 と似た感じですが、実際にテーブルを作って検索します async function f4(keys) { return new Promise((resolve) => db.serialize(() => { db.run('CREATE TABLE IF NOT EXISTS temp (hash TEXT NOT NULL)') db.run( `INSERT INTO temp (hash) VALUES ${Array(keys.length) .fill('(?)') .join(',')}`, ...keys ) const result = dbAll( db, 'SELECT hashes.id FROM temp LEFT OUTER JOIN hashes ON temp.hash = hashes.hash' ).then((rows) => rows.map((row) => row.id)) db.run('DROP TABLE temp', () => result.then((r) => resolve(r))) return result }) ) } 計測関数作成 実行時間を測る関数も作っておきます async function measureTime(label, func) { const start = process.hrtime() const result = await func() const end = process.hrtime(start) const timeStr = end[0].toString() + '.' + Math.round(end[1] / 100) .toString() .padStart(6, '0') console.log(label, result?.slice?.(0, 5), timeStr) } 試しに実行 length === 100 計測関数と被計測関数ができたので動かしてみましょう。 await measureTime('f1', () => f1(keys)) await measureTime('f2', () => f2(keys)) await measureTime('f3', () => f3(keys)) await measureTime('f4', () => f4(keys)) f1 [ 69, 8, 46, 49, 6 ] 0.076587 f2 [ 69, 8, 46, 49, 6 ] 0.017027 f3 [ 69, 8, 46, 49, 6 ] 0.006351 f4 [ 69, 8, 46, 49, 6 ] 0.153142 良い感じですね。length を 25565 にして計測してみましょう (25565 以上にするとテーブルを作る段階で死にます) length === 25565 f1 [ 6161, 14471, 17540, 20194, 22783 ] 2.1165730 f2 [ 6161, 14471, 17540, 20194, 22783 ] 0.752933 f3 undefined 0.175657 f4 [ 6161, 14471, 17540, 20194, 22783 ] 0.700146 f3 が死んでる・・・ length を色々変えて試したところ、keys の長さが 500 を超えると db.all が undefined を返すようになってしまっています sqlite3 の仕様なんですかね?UNION ALL の上限が 500 なのかな? 原因は良く分かりませんが、せっかく一番早い f3 がうまくいかないのは残念なので与えられた keys を 500 ずつに分割して実行する f3Alt 関数を作成します async function f3Alt(keys) { const keyss = [] const length = 500 for (let i = 0; i < keys.length / length; i++) { keyss.push(keys.slice(i * length, (i + 1) * length)) } return (await Promise.all(keyss.map(f3))).flat() } f1 [ 3162, 4337, 24559, 24876, 23262 ] 2.1619436 f2 [ 3162, 4337, 24559, 24876, 23262 ] 0.677855 f3Alt [ 3162, 4337, 24559, 24876, 23262 ] 0.586890 f4 [ 3162, 4337, 24559, 24876, 23262 ] 0.724264 うまくいきました!分割実行してなお f3 の UNION ALL 方式が一番早いですね 結論 100 回実行して箱ひげ図を作りました f4 の最低値がやたら低いのが気になりますが、最速としては f3Alt (UNION ALL) が一番なのではないかと思います 書くのが面倒なので普段使いでは f2 (WHERE IN) を使いますけどね おまけ 上記のコードを組み合わせたうまいこと動くものを置いておきます コード const sqlite3 = require('sqlite3') const db = new sqlite3.Database('./test.db') async function dbGet(db, ...args) { return new Promise((resolve) => db.get(...args, (err, result) => resolve(result)) ) } async function dbAll(db, ...args) { return new Promise((resolve) => db.all(...args, (err, result) => resolve(result)) ) } async function measureTime(label, func) { const start = process.hrtime() const result = await func() const end = process.hrtime(start) const timeStr = end[0].toString() + '.' + Math.round(end[1] / 100) .toString() .padStart(6, '0') console.log(label, result?.slice?.(0, 5), timeStr) } async function f1(keys) { return await Promise.all( keys.map((key) => dbGet(db, 'SELECT id FROM hashes WHERE hash = ?', key).then( (row) => row?.id ) ) ) } async function f2(keys) { const rows = await dbAll( db, `SELECT * FROM hashes WHERE hash IN (${Array(keys.length) .fill('?') .join(',')})`, ...keys ) const dict = {} for (const { id, hash } of rows) { dict[hash] = id } return keys.map((key) => dict[key]) } async function f3(keys) { return ( await dbAll( db, `SELECT hashes.id FROM (SELECT ? AS hash ${Array(keys.length - 1) .fill('UNION ALL SELECT ?') .join('')} ) AS temp LEFT OUTER JOIN hashes ON temp.hash = hashes.hash`, ...keys ) )?.map?.((row) => row.id) } async function f3Alt(keys) { const keyss = [] const length = 500 for (let i = 0; i < keys.length / length; i++) { keyss.push(keys.slice(i * length, (i + 1) * length)) } return (await Promise.all(keyss.map(f3))).flat() } async function f4(keys) { return new Promise((resolve) => db.serialize(() => { db.run('CREATE TABLE IF NOT EXISTS temp (hash TEXT NOT NULL)') db.run( `INSERT INTO temp (hash) VALUES ${Array(keys.length) .fill('(?)') .join(',')}`, ...keys ) const result = dbAll( db, 'SELECT hashes.id FROM temp LEFT OUTER JOIN hashes ON temp.hash = hashes.hash' ).then((rows) => rows.map((row) => row.id)) db.run('DROP TABLE temp', () => result.then((r) => resolve(r))) return result }) ) } async function createTable(hashes) { return db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS hashes ( id INTEGER UNIQUE NOT NULL PRIMARY KEY, hash TEXT UNIQUE NOT NULL )`) db.run( `INSERT OR IGNORE INTO hashes ( hash ) VALUES ${Array(hashes.length).fill('(?)').join(',')}`, ...hashes ) }) } async function main(length) { const crypto = require('crypto') const createHash = (num) => crypto.createHash('md5').update(num.toString()).digest('hex') const hashes = Array(length) .fill(0) .map((_, i) => createHash(i)) await createTable(hashes) const keys = Array(Math.ceil(length / 2)) .fill(0) .map(() => Math.ceil(Math.random() * length * 1.01)) .map(createHash) await measureTime('f1 ', () => f1(keys)) await measureTime('f2 ', () => f2(keys)) // await measureTime('f3 ', () => f3(keys)) await measureTime('f3Alt', () => f3Alt(keys)) await measureTime('f4 ', () => f4(keys)) } main(25565)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nightwatch で E2E テストを行う時に webpack-dev-server を自動で起動・停止させる

環境 Node.js@v15.3.0 Nightwatch@v2.0.6 webpack@v4.46.0 やりたいこと 実際の e2e テストを行う前に、ローカル環境で e2e テストスクリプトが正しく動作するか、確認したい。 ターミナルから webpack-dev-server を起動 → Nightwatch起動でもいいが、そうじゃない。 コマンド1つで webpack-dev-server起動 → Nightwatchによるe2eテスト実行 → webpack-dev-server停止までを自動化したい。 結論 以下の様にする 前提 webpack-dev-server 単体で、正しく動作する事 Nightwatch 単体で、正しく動作する事 ターゲットブラウザはChromeのみ ディレクトリ構成 /build  webpack.conf.json /test  /e2e   /specs    test.js   nightwatch.conf.js   nightwatch.js   runner.js Nightwatch 周り Nightwatch 設定 test/e2e/nightwatch.conf.js const webpackDevConfig = require('../../build/webpack.dev.conf') // webpack の設定を読み込み const E2E_DIR = 'test/e2e' // e2e テスト用ディレクトリ // Nightwatch の設定 // @see https://nightwatchjs.org/guide/configuration/ module.exports = { // 基本設定 src_folders: [`${E2E_DIR}/specs/*.spec.js`], output_folder: `${E2E_DIR}/.reports` // セレニウムサーバーの設定 selenium: { start_process: true, server_path: require('selenium-server').path, log_path: `${E2E_DIR}/.selenium-server-log`, host: 'localhost', port: 4444, cli_args: { 'webdriver.chrome.driver': require('chromedriver').path // Nightwatch 公式のパス指定だとエラーになる } }, // テストの設定 test_settings: { default: { // 起動 URL を webpack-dev-server のローカルホストに向ける launch_url: `http://${webpackConfig.dev.host}:${webpackConfig.dev.port}`, silent: true, desiredCapabilities: { browserName: 'chrome' } }, chrome: { desiredCapabilities: { browserName: 'chrome', javascriptEnabled: true, acceptSslCerts: true } } } } nightwatch ローダー test/e2e/nightwatch.js // @see https://nightwatchjs.org/guide/running-tests/nightwatch-runner.html require('nightwatch/bin/runner.js') nightwatch ランナー test/e2e/runner.js // @see https://github.com/Marak/colors.js require('colors') // コンソールを色づける // @see https://webpack.js.org/concepts/ const webpack = require('webpack') const merge = require('webpack-merge') const WebpackDevServer = require('webpack-dev-server') const webpackDevConfig = require('../../build/webpack.dev.conf') // webpack の設定を読み込み const E2E_DIR = 'test/e2e' // e2e テスト用ディレクトリ // 不要なwebpack設定を一時的に上書き let webpackConfig = merge(webpackDevConfig, { devServer: { client: { logging: 'none', overlay: false }, open: false, static: { watch: false } }, stats: 'none' // webpack-dev-server のすべてのログ出力を停止 }) // build const compiler = webpack(webpackConfig) let webpackDevServer = new WebpackDevServer({ ...webpackConfig.devServer, open: false }, compiler) // webpack-dev-server start webpackDevServer .start() .then(function () { // webpack-dev-server の起動ログ console.log(' Successfully '.bgGreen.black + ' webpack-dev-server'.blue + ' is running on ' + `'http://${webpackConfig.devServer.host}:${webpackConfig.devServer.port}'`.cyan) // @see https://github.com/moxystudio/node-cross-spawn const spawn = require('cross-spawn') // Nightwatch の起動。 // node コマンドライン引数として、nightwatchローダーである nightwatch.js と、 // nightwatch コマンドラインの引数 --config nightwatch.conf.js を指定する // @see https://nightwatchjs.org/guide/running-tests/nightwatch-runner.html const runner = spawn('node', [`${E2E_DIR}/nightwatch.js`, '--config', `${E2E_DIR}/nightwatch.conf.js`], { stdio: 'inherit' }) // 正常終了 runner.on('exit', function (code) { webpackDevServer.stopCallback(() => { // webpack-dev-server の停止ログ console.log(' Successfully '.bgGreen.black + ' webpack-dev-server'.blue + ' has been stopped.') process.exit(code) }) }) // 異常終了 runner.on('error', function (err) { webpackDevServer.stopCallback(() => { // webpack-dev-server の停止ログ console.log(' Successfully '.bgGreen.black + ' webpack-dev-server'.blue + ' has been stopped.') throw err }) }) Node.js 周り package.json package.json { "scripts": { "e2e-dev": "node test/e2e/runner.js" } } E2E テストスクリプト test/e2e/specs/test.spec.js // NightWatch API // @see https://nightwatchjs.org/api/ describe('Test demo', function () { it('Run demo', function (browser) { browser .url(browser.baseUrl) .assert.visible('#app') .end() }) }) ここまで。で用意は完了。 E2E テストの実行結果 ターミナルから npm run e2e-dev を投下 webpack-dev-server が自動起動。起動完了を待機。 起動完了したら、Nightwatch ランナーをコマンドラインで呼び出し e2e テストスクリプトを全て実行し終えたら、webpack-dev-server を自動で終了 console $ npm run e2e-dev > node test/e2e/runner.js Successfully started webpack-dev-server on http://localhost:8080 [Test demo] Test Suite ────────────────────────────────────────────── ℹ Connected to Selenium Server on port 4444 (2903ms). Using: chrome (98.0.4758.102) on WINDOWS. Running Run demo: ─────────────────────────────────────────────────────────────────────────────────────────────────── ⠋ Loading url: http://localhost:8080 DONE Compiled successfully in 6688ms 15:59:11 ℹ Loaded url http://localhost:8080 in 4133ms √ Testing if element <#app> is visible (68ms) OK. 1 assertions passed. (4.698s) Successfully webpack-dev-server stopped. 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsの通常関数のthisに関して

初めに 普段Node.jsを使用しており、通常関数のthisの基本的な仕様を勉強したためアウトプットのために記事を投稿する。 開発環境 ■ Node.jsのバージョン v11.1.0 ■ 使用OS Amazon Lunux 2 ① 基本的な例 const o = { name: 'taro', introduce() { return `My name is ${this.name}`; } } console.log(o); console.log(o.introduce()); 上記コードでは、oという定数にオブジェクトが格納されている。 以下は参考文献[1]より抽出したもの。 メソッドが呼び出されるときthisは、そのメソッドがプロパティとなっているオブジェクトを指しています。 こちらを拝借すると、ここで指しているメソッドとは以下にあたる。 introduce() そして、メソッドがプロパティとなっているオブジェクトとは定数oである。 そのため、thisはoをバインドするため以下に置き換えられる。 this.name ↓ o.name よって出力結果は以下となる。 { name: 'taro', introduce: [Function: introduce] } My name is taro ②オブジェクトのプロパティを代入した例 const o = { name: 'taro', introduce() { return `My name is ${this.name}`; } } const introduce= o.introduce; console.log(introduce=== o.introduce); console.log(introduce()); console.log(o.introduce()); 今回は、オブジェクトのプロパティを定数introduceに代入している。 出力結果は以下となる。 true My name is undefined My name is taro 代入した定数では、結果がundefinedとなっている。 これは、プロパティとなっているオブジェクトを特定できないため、thisがundefinedになっている。 ③ 関数が入れ子になっている場合 const o = { name: 'taro', external() { console.log(this.name); function internal() { console.log(this.name); } internal(); }, }; o.external(); 出力結果 taro undefined この場合、externalメソッドのthisは定数oをバインドしているが、internal関数内のthisは何もバインドしていない。 以下は参考文献[2]より抽出したもの。 ほとんどの場合、this の値はどのように関数が呼ばれたかによって決定されます (実行時結合)。これは実行時に代入によって設定することはできず、関数が呼び出されるたびに異なる可能性があります。 通常の関数では呼び出し方によってthisのバインド先が変わることが分かる。 internal関数の呼び出しはexternalメソッドで行っているため、thisがundefinedとなっている。 この問題は以下のような解決策がある。 const o = { name: 'taro', external () { let self = this; console.log(this.name); function internal() { console.log(self.name); } internal(); }, }; o.external(); externalメソッドのthisをselfという変数に代入して、undefinedとなっていたinternal関数内で使用している。 参考文献を確認すると、bind() メソッドやアロー関数等でthisのバインド先を変更できるようだが、上記コードのような解決方法も使用できる。 参考文献 [1] 初めてのJavaScript 第3版――ES2015以降の最新ウェブ開発 [2] MDN Web Docs
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Expressのデフォルトエラーハンドリングが作動しない??

なにが起こったか? app.use(( // eslint-disable-next-line @typescript-eslint/no-explicit-any err: any, req: express.Request, res: express.Response<BaseResponse.BaseApiResponse<{}>>, // eslint-disable-next-line @typescript-eslint/no-unused-vars next: any ) => { SentryLib.captureException(err) LoggerLib.debug(`${err}`) res.status(500).json({ data: {}, error: { message: 'standard error', code: 'ServerError' }, }) next() }) export default class Controller { /** * * @param {express.Request} req * @param {express.Response} res */ public static async get( req: express.Request< {}, AreaPublicApiResponse.GetApiResponse, any, {} >, res: express.Response, next: express.NextFunction ) { throw Error('エラーですよ') } } 上記のようにデフォルトエラーハンドリングの実装を行っていて、APIで強制的にエラーを出すように設定してみたがデフォルトのエラーハンドリングが機能しない 公式を確認してみる https://expressjs.com/ja/guide/error-handling.html app.get('/', (req, res, next) => { setTimeout(() => { try { throw new Error('BROKEN') } catch (err) { next(err) } }, 100) }) nextを実行しないとどうやらデフォルトハンドリングまで行ってくれないらしい Railsに慣れちゃってたので、エラーが起きたら自動的に検知してくれると勘違いしていた 修正版 export default class Controller { /** * * @param {express.Request} req * @param {express.Response} res */ public static async get( req: express.Request< {}, AreaPublicApiResponse.GetApiResponse, any, {} >, res: express.Response, next: express.NextFunction ) {     try { throw Error('エラーですよ') } catch (e) { next(e) // nextを実行 } } } 無事動くようになりました! nextが呼ばれないとどうなるか If the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging. An Express application can use the following types of middleware: ref: https://expressjs.com/en/guide/using-middleware.html リクエストが滞留してしまうということが書かれている 同期的なエラーはExpressでデフォルトでハンドリングしてくれるが、非同期関数で発生するエラーはハンドリングしてくれないので、リクエストが滞留してしまいサーバーダウンの原因にもなってしまう恐れも Express v 5.xでは非同期関数のエラーも自動でnext()が呼ばれるようになる Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error. For example: ref: https://expressjs.com/en/guide/error-handling.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Heroku で デプロイ エラー (~version not specified in package.json)

開発環境 対象 バージョン 対象 version Node.js v15.4.0 npm 7.0.15 発生している問題・エラー 作成途中だったアプリを一時中断して、Nuxt.jsの学習をしていた。 久しぶりに途中だったアプリをgit push heroku mainをしたら、下記のエラーが出るようになってしまった。 どうやら、Node version not specified in package.jsonという部分が問題のようだった。 これは、Nodeのバージョンがpackage.jsonに記述されていないことらしい。 以前は普通にデプロイ出来てたのに、なぜこうなってしまったかは分からなかった。 デプロイログ. remote: -----> Build failed remote: remote: We're sorry this build is failing! You can troubleshoot common issues here: remote: https://devcenter.heroku.com/articles/troubleshooting-node-deploys remote: remote: Some possible problems: remote: remote: - Node version not specified in package.json remote: https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version remote: remote: Love, remote: Heroku remote: remote: ! Push rejected, failed to compile Node.js app. remote: remote: ! Push failed remote: ! remote: ! ## Warning - The same version of this code has already been built: 2e1b37e888e9ad2a5e50da16618bf37879e28f8a remote: ! remote: ! We have detected that you have triggered a build from source code with version 2e1b37e888e9ad2a5e50da16618bf37879e28f8a remote: ! at least twice. One common cause of this behavior is attempting to deploy code from a different branch. remote: ! remote: ! If you are developing on a branch and deploying via git you must run: remote: ! remote: ! git push heroku <branchname>:main remote: ! remote: ! This article goes into details on the behavior: remote: ! https://devcenter.heroku.com/articles/duplicate-build-version remote: remote: Verifying deploy... remote: remote: ! Push rejected to . remote: To https://git.heroku.com/~.git ! [remote rejected] main -> main (pre-receive hook declined) error: failed to push some refs to 'https://git.heroku.com/~.git' 解決手順 (https://devcenter.heroku.com/articles/troubleshooting-node-deploys) の「Other Available Runtimes」 を参考にして package.jsonに"engines": {~}を加えて、mainブランチにpushしたら解決できた。 今回の場合は、node.jsにエラーメッセージが出てたので、"node": "15.4.0" を加えた。 package.json { "name": "example-app", "description": "a really cool app", "version": "1.0.0", "engines": { "node": "15.4.0" } } 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む