GraphQL の startCursor/endCursor は nullable でもよい

GraphQL で ページネーションを実装するとき、Relay GraphQL Cursor Connections Specification に則ると、 startCursor/endCursor は non-null にする必要がある。

GraphQL Cursor Connections Specification

PageInfo must contain fields hasPreviousPage and hasNextPage, both of which return non-null booleans. It must also contain fields startCursor and endCursor, both of which return non-null opaque strings.

ただ、要素が空の場合は指し示す cursor 自体存在しないので “” のような空の文字列を返すことになり、これがちょっと気持ち悪い。

2019年頃から startCursor/endCursor は nullable にしてはどうかという issue が経って議論されているんだけど、最近どうやら進展があって nullable が許容された様子。 現実的には nullable で実装されていることが多いので、仕様もそっちに寄せましたみたいな内容。

https://github.com/facebook/relay/pull/2655#pullrequestreview-857972245

It's clear to me that startCursor/endCursor should be nullable, are treated as nullable by Relay, and are generally implemented in the community as nullable. The specification should reflect this de facto reality.

ドキュメント自体はまだ更新されていない様子だけど、そのうち多分更新されそう。

2021年競馬振り返り

2021年4月に初めて競馬をやったんだけど、未だに毎週を楽しみに続けている(きっかけとなったウマ娘はもはやプレイしていなくてただの競馬おじさんに成り果てている)。今日今年最後のG1レースも終わったのでこの1年の収支を振り返ってみる。

最終収支

f:id:magaming:20211229211726p:plain

これに12/26, 12/28 のレースの収支を加えると購入「126,800」, 払戻「221,800」になって回収率はなんと「175%」 となる。 競馬漫画「ウィナーズサークルへようこそ」では、回収率120~150%の人間は通称「馬券仙人」と呼ばれ、これは競馬ファンのわずか0.09%にあたり、今の仕事を辞めて、競馬予想に専念する者もでてくる。とされているらしい。

tonarinoyj.jp

競馬予想師初めて、オンラインサロン作って、そのうち映画でも作って信者にチケット売ってもらおうと思ったけど、まぁ続けていく以上最終的に回収率は75%付近に落ち着くはずなので、来年も趣味で無理のない範囲で楽しく続けていこうと思います。

browserslist と UA を比較するための正規表現を生成するグッズ browserslist-useragent-regexp

browserslist でサポートブラウザを定義しているんだけど、 Sentry で browserslist にないブラウザの場合エラーを送らないようなことがしたいなと考えていた。


browserslist では chrome 96 みたいにバージョンが定義されているけど、 UAMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 みたいな形になっている。比較するのには正規表現書いたりする必要がありそうで、なんかいいグッズがないかなと探していた。


で、browserslist-useragent-regexp というbrowserslistを元にUAと比較するための正規表現を生成するグッズがあるのを見つけた。


使ってみるとこういう一見異常な正規表現が出力される。

module.exports = /((CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS)[ +]+(10[_.]3|10[_.]([4-9]|\d{2,})|(1[1-9]|[2-9]\d|\d{3,})[_.]\d+|11[_.]0|11[_.]([1-9]|\d{2,})|11[_.]2|11[_.]([3-9]|\d{2,})|(1[2-9]|[2-9]\d|\d{3,})[_.]\d+|12[_.]0|12[_.]([1-9]|\d{2,})|12[_.]5|12[_.]([6-9]|\d{2,})|(1[3-9]|[2-9]\d|\d{3,})[_.]\d+|13[_.]0|13[_.]([1-9]|\d{2,})|13[_.]7|13[_.]([8-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})[_.]\d+|14[_.]0|14[_.]([1-9]|\d{2,})|14[_.]4|14[_.]([5-9]|\d{2,})|14[_.]8|14[_.](9|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})[_.]\d+|15[_.]0|15[_.]([1-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})[_.]\d+)(?:[_.]\d+)?)|(CFNetwork\/8.* Darwin\/16\.5\.\d+)|(CFNetwork\/8.* Darwin\/16\.6\.\d+)|(CFNetwork\/8.* Darwin\/16\.7\.\d+)|(CFNetwork\/8.* Darwin\/17\.0\.\d+)|(CFNetwork\/8.* Darwin\/17\.2\.\d+)|(CFNetwork\/8.* Darwin\/17\.3\.\d+)|(CFNetwork\/8.* Darwin\/17\.\d+)|(Edge\/(92(?:\.0)?|92(?:\.([1-9]|\d{2,}))?|(9[3-9]|\d{3,})(?:\.\d+)?))|((Chromium|Chrome)\/(61\.0|61\.([1-9]|\d{2,})|(6[2-9]|[7-9]\d|\d{3,})\.\d+|79\.0|79\.([1-9]|\d{2,})|([8-9]\d|\d{3,})\.\d+|91\.0|91\.([1-9]|\d{2,})|(9[2-9]|\d{3,})\.\d+)(?:\.\d+)?)|(Version\/(10\.1|10\.([2-9]|\d{2,})|(1[1-9]|[2-9]\d|\d{3,})\.\d+|11\.0|11\.([1-9]|\d{2,})|(1[2-9]|[2-9]\d|\d{3,})\.\d+|12\.0|12\.([1-9]|\d{2,})|(1[3-9]|[2-9]\d|\d{3,})\.\d+|13\.0|13\.([1-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})\.\d+|14\.0|14\.([1-9]|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})\.\d+|15\.0|15\.([1-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})\.\d+)(?:\.\d+)? Safari\/)|(Firefox\/(78\.0|78\.([1-9]|\d{2,})|(79|[8-9]\d|\d{3,})\.\d+|91\.0|91\.([1-9]|\d{2,})|(9[2-9]|\d{3,})\.\d+)\.\d+)|(Firefox\/(78\.0|78\.([1-9]|\d{2,})|(79|[8-9]\d|\d{3,})\.\d+|91\.0|91\.([1-9]|\d{2,})|(9[2-9]|\d{3,})\.\d+)(pre|[ab]\d+[a-z]*)?)/;

これだけ見るとわけわからんという感じだけど、少なくとも BrowserStack で古いバージョンのブラウザでいくつか試してみたところ普通に動いてそう。


Sentry だけではなく、サポート外のブラウザならメッセージを出す、みたいなこととかに使えるのかも。

ゲーミングディスプレイ買った

動機

数ヶ月前からめちゃめちゃに Apex をやっているんだけど、ずっとリフレッシュレートが 75Hz のディスプレイ1でプレイしていた。

www.lenovo.com


ゲーミングディスプレイにして世界が変わった!みたいな発言をインターネットでよく見るので前から 144Hz 以上のディスプレイが気になっていたんだけど、ブラックフライデーでめっちゃ安くなっていたので思い切って買ってみた。

www.lenovo.com

lenovo にしたのは、今持ってるディスプレイとデザイン合わせたいからだけでそれ以上の理由は特にない。

ちなみに今見たらブラックフライデー終わっても新たにサイバーマンデーセールが始まって価格は据え置きなので、多分一生安くなっていると思う。

使用感

なんかヌルヌルする!!!!

劇的な変化はない。若干当て感良くなったのと気がするのと、インファイト強くなった気がするけどあくまで気がする程度。

そもそももう30過ぎなので反射神経のほうが付いてこないのかもしれない。

無情。



  1. ちなみにこれは仕事用で、USB Type-C に対応していて、外部電源供給容量が十分だったので買った。

MFA アプリのデータが消滅すると破滅する

もう4年くらい iPhone7 を使っていたんだけど、 最近 Lightning 端子の接触が悪くケーブルの下に何か物をおいて角度を付けないと充電できないという不便極まりない状態だったので iPhone SE(第二世代)に機種変更した。

クイックスタート機能でデータ移行をしていたところ、残り4分のところで一生進まずどうしたものかと思っていたけど、写真とかメールとかの重要なデータは移行されていそうなのでまぁいいかと強制停止させた。

で、よく見たら一部のアプリのデータがちゃんと移行されていなかった。そのうち、MFA認証に使っている Google Authenticator のデータが消滅しているのがめっちゃ困った。


  • AWS(IAMユーザ)

    • 自分ではどうすることもできないので, 管理者の人に頼んで一旦 MFA 無効にしてもらってログインして再設定
  • Amazon

  • GitHub

    • 同じく他のPCにセッション残っていたので再設定
    • こちらは電話番号登録していたらそちらに認証コード送ることもできそうなのでなんとかなりそう
  • Facebook

    • 同じく他のPC(以下同文)
    • そもそも使ってないので消してもいいんだけど, 昔 Facebook でアカウント登録したサービスがあってそれが何か把握していないのでうかつに消せない…


すべてが無に返されたので、他にどのサービスで MFA 有効にしてたのかすら把握できていない状況…

多分しばらくしてなにかのサービスにログインしようとしたら、 MFA 要求されて積みそう。


調べてみると同じ事象に遭遇した人結構居るようで、Mirosoft Authenticator ならマイクロソフトアカウントにデータ保存してくれるらしいのでそっちに移行しようかな…と思っている。

ascii.jp

npm パッケージを beta 版で publish する

開発中の npm パッケージを publish して動作確認する必要があったのでその時の手順メモ.

バージョン指定

npm パッケージの package.jsonversion を, 1.0.0-beta.0 のように末尾に-beta-? が付くように書き換える.

またはnpm version premajor --preid betaコマンド打つと上記と同じような感じに書き換えてくれる.

https://docs.npmjs.com/cli/v6/commands/npm-version

特にbetaという文字列でなければいけないということではないので, nextでもnightlyでも開発中バージョンとわかるものであれば良さそうだけど様々なリポジトリ見る限りはbetaが多そう?

publish

npm publish --tag beta のようにタグを指定して publish する.

ちなみに, タグを付けないと default でlatest タグが設定される.

https://docs.npmjs.com/cli/v6/commands/npm-publish

使うとき

npm install hogehoge@beta のようにタグ指定でインストールすればよい.

またはnpm install hogehoge@1.0.0-beta.0 のようにバージョン直指定でもよい.

在宅ワーク中に家族に会議中だとアピールするライフハック

現在在宅ワーク中なわけだけど, 家族にミーティング中ですとアピールするために, ミーティングはいい感じのランプを点灯させたりしている。

f:id:magaming:20211005194559j:plain
こんなやつ

ただミーティング始まる/終わったときに手動でON/OFFするの面倒なので, 自動化してみた。

事前準備

まずは遠隔で操作できるように, スマートプラグを用意。

TP-Link のやつが安いのでそれにした。IFTTT 対応しているなら別なやつでもよさそう。Amazon でポチったら速攻届いたので, Webhook の設定して HTTP request 投げて点いたり消えたりしたのを確認。

www.wassyoi-hack.com

Chrome 拡張を作る

弊社ではミーティングに Google Meets を使用しているので, meet.google.com ドメインを開いたときにランプを点灯, 閉じたら消灯, みたいにしたい。今回はそれ用の Chrome 拡張を作った。どうやら chrome.tabs API を使えば現在開いているタブ一覧とかを取得できるので, それを使ってみる。

developer.chrome.com

manifes.json はこんな感じ。

{
  "manifest_version": 2,
  "name": "Google Meets 開いたらランプつけるくん",
  "description": "Google Meets 開いたらランプつけてくれます",
  "version": "1.0",
  "background": {
    "scripts": ["event.js"],
    "persistent": false
  },
  "permissions": ["tabs", "*://maker.ifttt.com/*"]
}

permissions に ifttt のドメインを指定しているのは, こうしないと CORS エラーで怒られるため。あとは適当に js 本体を用意。

const googleMeetsDomain = "meet.google.com";
const turnOnUrl =
  "https://maker.ifttt.com/hogehogehogel";
const turnOffUrl =
  "https://maker.ifttt.com/fugafugafuga";

var isTurnOn = 0;

function watchTabs() {
  chrome.tabs.query({}, (tabs) => {
    const isOpeningMeets = tabs.some((tab) => {
      return tab.url.match(/meet.google.com/);
    });

    if (isOpeningMeets && !isTurnOn) {
      request(turnOnUrl);
      isTurnOn = 1;
    }

    if (!isOpeningMeets && isTurnOn) {
      request(turnOffUrl);
      isTurnOn = 0;
    }
  });
}

function request(url) {
  var req = new XMLHttpRequest();
  req.open("GET", url);
  req.send();
}

chrome.tabs.onUpdated.addListener(watchTabs);
chrome.tabs.onRemoved.addListener(watchTabs);

chrome.tabs.query で全タブの url をなめて, Meets の url が含まれていれば ON, 含まれていないなら OFF する感じ。onRemoved はタブを閉じたとき、onUpdated はタブが更新されたときのイベント。Chrome 自体を閉じたときはこれだとダメだけど, あんまり困ってないのでまぁよいか, となっている。

実際の様子

f:id:magaming:20211006201118g:plain

Meets だけじゃなくて, 例えば youtube でご機嫌な動画を流したときにミラーボールを点灯するとか, 仕事中に Twitter を見始めたら吹き矢が飛んでくる, みたいなことに応用が効きそう。