Notes C API & C++11+ & ReactiveX & Qt #2

リアクティブ・エクステンション(Reactive Extensions、ReactiveX、RX)は奥が深く、勉強を始めてから日の浅い私もまだまだわからないことだらけです。ですが、初級レベルくらいならある程度書けるようになってきたので、Notes APIを例に、RXを適用するとこんな風になるというところをご紹介してみます。

ここでは、サーバ名を取得する関数「NSGetServerList」関数を使って、一般的な方法と、RXを使った方法とを比較してみます。NSGetServerList関数については、昨年書いたこちらをご覧下さい。

chiburusystems.hatenablog.com

これをDirというクラスの静的メソッドとして実装したとします。そして、そのメソッドを呼び出して、サーバ名のリストを取得する側のコードを書いてみます。私のように、20年以上オブジェクト指向でプログラムを書いてきた場合、次のようになるでしょう。

namespace nx {
using namespace chiburu::noteslib;
}

QList<nx::String> serverList = nx::Dir::getServerList();
QTextStream sout(stdout);
foreach (nx::String server, serverList) {
  sout << server.toQString() << endl;
}

名前空間やクラス名などのこまかいところはさておき、getServerListで取得したサーバ名リストをQList<nx::String> serverListに保管し、標準出力にサーバ名を出力します。極めて一般的な書き方ではないでしょうか。 (※nx(=chiburu::noteslib)名前空間がついたクラスが独自のもので、特にnx::StringはLMBCS文字列を表しています。)

このnx::Dir::getServerListメソッドを、リアクティブ・エクステンションで実装し直すと、こんな書き方になります。

QTextStream sout(stdout);
nx::Dir::getServerListStream()
  .subscribe([&](nx::String server) {
    sout << server.toQString() << endl;
});

&{...}という書き方は、C++11以降で使える「ラムダ式」で、JavaScriptでいうクロージャのようなもので、リアクティブ・エクステンションに直接関係はないですが、これがないとC++でRXは立ちゆかないでしょう。

getServerList関数は、getServerListStreamと名前を変えました。シグネチャは次のようになります。

#include <rxcpp/rx.hpp>
namespace Rx {
using namespace rxcpp;
}

Rx::observable<nx::String> nx::Dir::getServerListStream(nx::String port = nx::String());

Rx::observable?いきなり出てこられても困りますが、ここでは、「LMBCS文字列を流してくるもの」というふわっとした感じで捉えておいて下さい。「流してくる」ので「捕らえれば」サーバ名を取得できる・・・という具合です。じゃあ、どこで「捕らえる」か?「subscribe」で「捕らえます」。「subscribe」は「observable」のメソッドです。「observable」が流すものを、「subscribe」で捕らえて、指定したラムダ式や関数オブジェクトなどで処理します。

リアクティブ・エクステンションを使うと何が嬉しいか?検索するといろいろ議論になっていますが、ここでは局所的に捉えてみます。

今回のNSGetServerList関数をラップする場合、取得したサーバ名を返すとき、サーバ名が一括で返ってきてしまうので、リストという形でまとめ上げ、それを返すことになります。テンプレートを使ったとしても、すべてのサーバ名を「なんらかのコンテナオブジェクト」に収めてから返すことに変わりはありません。

リアクティブ・エクステンションを使って、取得したサーバ名を、順次呼び出し側に提供するようにすると、コンテナオブジェクトを強要することがなくなりました。呼び出し側は、サーバ名の扱い方を自分で決めることができるようになります。これだけでも十分すごいことだと、実装してみて感じました。

今回のようにNotes APIを使った例では、同期的なプログラムが大半を占めますが、この仕組みは非同期的なプログラムでも使えます。特にHTTPリクエストやUI更新など非同期を多く使う場面では、リアクティブ・エクステンションは威力を発揮するようです。同期、非同期に関係なく使えることはとても大きいと感じています(実際には意識する必要がでてきますが)。

Notes C API & C++11+ & ReactiveX & Qt #1

仕事でAngularを使い始めました。Angularは、ReactやVueなどと比較されるSPAフレームワークです。開発にはJavaScript回りがわかればいいのですが、TypeScriptから入ることをお勧めされます。

https://angular.io/

www.typescriptlang.org

AngularJS(1.x)からAngular(2.x~)に移行したとき、TypeScriptが体に合わず、しばらく遠ざかっていましたが、世の中SPAなしでは生きていけず、またTypeScriptの構文がES6(ES2015)にも通じることから、意を決して取り込むことにしました。

ただ、後1つクリアしなければならないハードルがあります。RX(Reactive Extensions、ReactiveX)です。

http://reactivex.io/

Angularのチュートリアルに取り組んでいると、奇妙な言葉に遭遇します。

https://angular.io/tutorial/toh-pt4#observable-heroservice

「Observable」?オブザーバブル?監視可能なもの?

いろいろ端折りますが、乱暴にまとめれば「あらゆるデータを監視可能なストリーム(流れ)として扱えるようにした仕組み」で、これをAngularは積極的に取り込んでいるので、RX学習はほとんど必須科目になっています。

RXは大変奥が深いようで、私の学習レベルもまだまだですが、七転八倒の末、「なるほどこれがRXか!」というパラダイムシフトが起きて以来、手放せないロジックになっていることは確かです。今までFor文、コールバック、Promiseなどが当たり前だった頭の回路が、RXなしでは成立しないと言っても過言ではないです。私がパラダイムシフトを起こすために助けになったのは、この本のおかげです。

関数型リアクティブプログラミング (Programmer's SELECTION)

関数型リアクティブプログラミング (Programmer's SELECTION)

しかし待てよ、と。JavaScriptでこんな便利な考え方があるなら、C++の世界でも使えないことはなかろう、と。RXはあらゆる言語で展開されているようです。

ReactiveX - Languages

C++もRxCppという名前で展開されていました。となれば、以前から取り組んでいるNotes C APIラッパーライブラリを、RXや最新のC++言語使用を取り入れて、アップデートしてもよいのではないだろうか・・・、と思い立ちました。

もちろん、ベースとなるのは国内唯一のNotes C API本であるこの本です。

Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミング

いまだにバイブルであることに変わりはありません。これに、C++11以降の言語仕様、ReactiveXの仕組み、QtマルチプラットフォームC++GUIフレームワークを組み合わせて、V10時代を迎えるNotes/DominoのC APIを使いやすくするライブラリを目指していきます。

W64APIにまたしても!DNParseの落とし穴

いつぞやのNSFItemInfoNext関数に続き、またしてもW64で正常に動かないC APIを発見しました。今回は、DNParse関数です。まず、DNParse関数とはなんぞや?というところから。

DNParse関数の接頭辞、「DN」は「Distinguished Name」の略で、識別名を表します。Notesでいうところの、

CN=Taro Yamada/O=Acme/C=JP

というID名を指します。ユーザ名、サーバ名などがそれにあたります。実際に捌ける要素はこれだけにとどまらないようですが、ここでは割愛します。ここでのポイントは、「DN」を冠した関数は識別名を操作する関数ということです。

DNParseは、その識別名をパーツごとに分解することができます。例えば、前出の例を元に分解すると、

Common Name => Taro Yamada
Org Name => Acme
Country Name => JP

となります。ヘッダーでは、DNParse関数は次のように定義されています。

#include <dname.h>

STATUS LNPUBLIC DNParse(
    DWORD Flags,
    const char far *TemplateName,
    const char far *InName,
    DN_COMPONENTS far *Comp,
    WORD CompSize);

FlagsとTemplateNameは0を渡します。 InNameに分解したい識別名へのポインタを入力します。 続く「DN_COMPONENTS」という構造体に、分解された情報が入ってくるので、事前にDN_COMPONENTS変数を用意して、そのポインタをCompに、変数サイズをCompSizeに与えれば取得できます。 問題なく分解できれば、NOERRORを返します。

DN_COMPONENTS dn;
STATUS result = DNParse(0, 0, "CN=admin/O=acme", &dn, sizeof(dn));

ヘッダーでDN_COMPONENTSを見ると、Ver3はここまで、Ver4はここから、みたいなコメントがあり、拡張が繰り返されてきたことが伺えます。

#include <dname.h>

typedef struct {
  DWORD Flags;                    /* Parsing flags */
/* (中略) */
  WORD CLength;                   /* Country name length */
  char far *C;                    /* Country name pointer */
  WORD OLength;                   /* Organization name length */
  char far *O;                    /* Organization name pointer */
  WORD OULength[DN_OUNITS];       /* Org Unit name lengths */
  /*  OULength[0] is rightmost org unit */
  char far *OU[DN_OUNITS];        /* Org unit name pointers */
  /*  OU[0] is rightmost org unit */
  WORD CNLength;                  /* Common name length */
  char far *CN;                   /* Common name pointer */
  WORD DomainLength;              /* Domain name length */
  char far *Domain;               /* Domain name pointer */

  /* Original V3 structure ended here.  The following fields were added in V4 */

  WORD PRMDLength;                /* Private management domain name length */
  char far *PRMD;                 /* Private management domain name pointer */
/* (中略) */
} DN_COMPONENTS;

実際にDNParseをDomino Win32bitで動かすと、DN_COMPONENTSには以下のような情報が返ってきます。

f:id:takahide-kondoh:20180604101604p:plain

ハイフンを挟んで左が文字数、右が文字列へのポインタです。Country name、Org name、Common nameのそれぞれに文字数2、文字数4、文字数11(0x0B)と、文字列が格納されているポインタが格納され、Org Unit name0〜3にはデータがないのがわかります。

全く同じコードをWin64用にコンパイルして動かすと以下のようになります。

f:id:takahide-kondoh:20180604122823p:plain

「あれれ〜おかしいなあ〜。ポインタっぽい値が文字数変数に入り込んでいるぞ〜。」という感じになっています。

このズレは何なのか、いろいろ考えてみました。単純に、メモリ配置的に、ポインタが4バイトから8バイトになったというだけでは説明が付きませんでした。試行錯誤の結果、国名(C)、組織名(O)共通名(CN)までの変数であれば、次のようにパディングすることで解決しました。

#ifdef W64

typedef struct {
  DWORD Flags;                    /* Parsing flags */
/* (中略) */
  WORD CLength;                   /* Country name length */
  WORD c_padding; // パティング
  char far *C;                    /* Country name pointer */
  WORD OLength;                   /* Organization name length */
  WORD o_padding[3]; // パティング
  char far *O;                    /* Organization name pointer */
  WORD OULength[DN_OUNITS];       /* Org Unit name lengths */
  /*  OULength[0] is rightmost org unit */
  char far *OU[DN_OUNITS];        /* Org unit name pointers */
  /*  OU[0] is rightmost org unit */
  WORD CNLength;                  /* Common name length */
  WORD cn_padding[3]; // パティング
  char far *CN;                   /* Common name pointer */
  WORD DomainLength;              /* Domain name length */
  char far *Domain;               /* Domain name pointer */

  /* Original V3 structure ended here.  The following fields were added in V4 */

  WORD PRMDLength;                /* Private management domain name length */
  char far *PRMD;                 /* Private management domain name pointer */
/* (中略) */
} DN_COMPONENTS;

#else

/* (オリジナルのDN_COMPONENTS定義をここに) */

#endif

CLengthとCの間にWORD一つ分、OLengthとOの間にWORD3つ分、CNLengthとCNの間も同じく3つ分パディングすると、以下のように正しく変数に格納されたのが確認できました。

f:id:takahide-kondoh:20180604135457p:plain

Domain以下の変数に対しては試せていませんが、ズレているのであれば、逐次直していくしかないでしょう。

まとめ

Notes/Dominoは登場初期からマルチプラットフォーム指向で、C APIも早い段階から提供され、Notes/Dominoのバージョンとともに更新されてきました。ただ残念なことに、C APIに目を向ける人口が少ないせいか、時々このようにお粗末なところが放置され、それを指摘するような利用者サイドも皆無となっています。 また、こういう関数が放置されているのにWin64 Dominoサーバがほぼ問題なく動いているのは、Undocumentedな関数で実装されていることは想像に難くなく、Windows ToolのDependency Walkerにかければそれっぽい関数名が浮かび上がります。 C APIのドキュメントもきちんと整備し、Documentableになった関数からユーザに開放してくれることを切に願うばかりです。

DominoサーバにOAuth2認証機能をアドインする「DOAP」

先日、Facebookページで開発を表明したDominoのOAuth2アドインですが、一部の機能を残して0.1.0-betaバージョンが完成しました。

名称は「DOAP」Domino OAuth2 Providerの略です。

このDOAPは、Dominoにアカウントを持っているユーザが、他のWebサービスからDominoのリソースにアクセスしたいときに、OAuth2の仕組みでシームレスに「認可」することができます。

通常、DominoはBasic認証かフォーム認証で、REST APIでさえこの認証が必要になります。 Basic認証の場合は常にユーザ名、パスワードをどこかに保持しておかなければなりません。 フォーム認証の場合も同様で、さらにセッションが切れたとき、ログイン画面をHTMLで返してきます。 このとき厄介なのが「HTTP/1.1 200 OK」と、認証が切れていることをリターンコードだけでは判断できないのです。

これを一気に解決する手立てがOAuth2認証であり、その実装が「DOAP」です。

この問題が解決されると、OAuth2に対応したWebサイト構築言語の間口が途端に開けることになります。 Node.js、PHPRubyPython、.NETなどなど。 Dominoを「認可サーバ」「リソースサーバ」という形で、言語にとらわれずに活用することが可能になります。

構想こそ長くボヤッとしていたものの、「もしやできるのは?」と思い立ったのが、このGW前。 1ヶ月ほどでこのような形になったのも、応援して下さった方々のおかげです。

Lotus Notes R3.0Jに出会い、およそ25年。 C APIC/C++で開発する人が周りにいない中、C APIで初めて製品を構築したのが約5年前。 それからも2つの製品開発に応え、このOAuth2開発に至りました。

現在、関係者とDOAPの公開方法について模索しているところです。 広く使ってもらいたいところですが、認証というセンシティブな一面もあるので、なかなか頭を悩ませています。 正式な発表は別のところからになりますが、その折にはここでもリンクさせていただきます。

NSFItemInfoNext関数を64bit Windows Dominoサーバで使用するとクラッシュする

私が、とあるプロジェクトでDominoサーバのアドインを作ったとき、奇妙な現象に悩まされた。

その現象というのは、NSFItemInfoNextというNotes C APIの関数を使うと、32bit WindowsのNotesクライアントとDominoサーバ、64bitのMacOS、32bit Linuxではまったく問題がないのに、64bit WindowsのDominoサーバでのみクラッシュするという事象である。

そもそも「NSFItemInfoNext」とは何なのか。

NSFItemInfoNext関数は、通常NSFItemInfo関数とペアで使用する。「NSFItem○○」という関数はアイテム(フィールドと同義)に関する操作を行う種類のもので、NSFItemInfoはアイテムの情報を取得するのに使用する。アイテムは、名前、型、値、属性の4つで構成されていて、NSFItemInfoは属性以外の情報を取得できる。アイテムは文書中名前(フィールド名)で区別されるため、通常1文書に1つだが、1アイテム中に保持できる値の大きさが限られているため、同名アイテムが複数保管されることがある。その時、2つめ以降の同名アイテムを取得するのに使われるのが、NSFItemInfoNext関数というわけである。

例えば、次のようにコーディングになる。

STATUS error = NSFItemInfo(hNote, "Body", strlen("Body"), &bItem, &wType, &bValue, &dwLength);
if (ERR(error) != ERR_ITEM_NOT_FOUND) {
  if (error) return;
  bPrevItem = bItem;
  error = NSFItemInfoNext(hNote, bPrevItem, "Body", strlen("Body"), &bItem, &wType, &bValue, &dwLength);
}

バイト数が決まっている数値型(NUMBER)、日時型(TIMEDATE)やシンプルなテキストデータくらいであれば、同名アイテムを2つ以上に分けて保存することはめったにないが、64キロバイトを超えるデータは文書に保存できないため、同名アイテム保存を使用する。Notesクライアントは、約40キロバイトを目安に同名アイテムを2つ以上に分けて保存する。

このように、NSFItemInfoNext関数は何か特殊な関数というわけではなく、APIとしては極めてベーシックな存在の関数なのだが、それだけにDominoサーバx64Winでクラッシュし、その原因がNSFItemInfoNextだとわかった時は、首をひねるばかりだった。

Notes C APIを使用したコードをコンパイルする場合、Windows 32ビットでは、少なくとも次の3つを指定する。

-DW -DW32 -DNT

Windows 64ビットでは、これらに加えて、以下の3つも必要になる。

-DW -DW32 -DNT -DW64 -DND64 -D_AMD64_

これら以外に識別子の過不足がないか調べてみたが、特に問題ない。

ネット上で調べているうちに、興味深い記述を見つけた。NSFItemInfoNext関数は以下のような引数を取る。

STATUS LNPUBLIC NSFItemInfoNext (
  NOTEHANDLE hNote,
  BLOCKID NextItem,
  const char far *Name,
  WORD NameLength,
  BLOCKID far *retbhItem,
  WORD far *retDataType,
  BLOCKID far *retbhValue,
  DWORD far *retValueLength
);

その記事では、この関数をLotusScript内から呼び出した時に、BLOCKID型の引数を倍精度小数点数(double型)の幅8バイトで指定する必要があるということだった。

C APIの世界では、BLOCKIDはプールハンドル(4バイト)とブロックハンドル(2バイト)の2つの値を持つ6バイト構造体だ。これを8バイトにするには、2バイトのブロックハンドルを4バイトにすることになる。

Notes C APIでブロックハンドルを定義しているのは、global.hヘッダーファイルだ。

// global.hの1076行目
typedef WORD BLOCK;

これを次のように書き換える。

#ifdef W64
typedef DWORD BLOCK;
#else
typedef  WORD BLOCK;
#endif

こうすることで、NSFItemInfoNext関数はクラッシュしなくなり、 BLOCKIDを使用するほかの関数も問題なく動作している(ように見える)。この手法が他の関数に本当に影響はないのか予断を許さないが、64bit WindowsのDominoサーバでクラッシュに困ったら、NSFItemInfoNext関数に起因していないか調べ、上記方法を試してみるのもよいかもしれない。

画面を更新してもチャットの履歴を表示できるようにする

リアルタイムでやり取りしたチャットメッセージは、通常一過性のもので、Web画面を更新してしまうと消えてなくなります。幸い、この連載で扱っているチャットデータは、Notesデータベースに記録されています。この情報を再取得すれば、チャットのやり取りを画面に再現することができます。

チャットデータをリロードするタイミングは、画面のロード時になるので、onLoad時にイベントを発行します。

// public/javascripts/chat.js

(function()
{
  // クライアント側のソケットを初期化します。
  var socket = io.connect('http://localhost:3000');

// (中略)

  // リフレッシュメッセージを受信します。
  socket.on('refresh', function(data)
  {
    var range = data.range.match(/items (\d+)-(\d+)\/(\d+)/);
    var start = parseInt(range[1]);
    var end = parseInt(range[2]);
    var allCount = parseInt(range[3]);
    if (start === 0)
      $('#output').empty();
    data.items.forEach(function(item)
    {
      var content = item['$120'];
      var message = content.match(/(.*) \(/);
      var username = content.match(/ \((.*)\)/);
      $('#output').prepend(
        '<p>'
          + '<strong>' + username[1] + '</strong>: '
        + message[1] + '</p>'
      );
    });
    if (end + 1 < allCount)
      socket.emit('reload', {'start': end + 1, 'count': countUnit});
  });

  // チャットの履歴を取得します。
  var countUnit = 10;
  socket.emit('reload', {'start': 0, 'count':countUnit});

})()

chat.jsで定義している無名関数がロード時に実行されるので、ここでメッセージの履歴を取得します。最後の方にある「socket.emit('reload', ...)」が、Node.jsのWebSocketに対してメッセージのリロード要求をしています。

前半部の「socket.on('refresh', ...);」については後ほど補足します。

Webクライアントからreloadメッセージが送信されると、Note,js(WebSocket)ではそのままカスタムクラス「NodeChat」の「reload」メソッドを呼び出します。

// mySocket.js

// (中略)

module.exports = function(server, sessionStore, User)
{
// (中略)

    // チャットの内容をリロードします。
    socket.on('reload', function(input)
    {
      getUser(socket)
      .then(function(user)
      {
        var nodeChat = new NodeChat(user);
        return nodeChat.reload(input);
      })
      .then(function(data)
      {
        socket.emit('refresh', data);
      })
      .catch(function(err)
      {
        console.log(err);
      });
    });
  });

  return io;
};

NodeChat.reload処理が無事に終わると、送信元(Webクライアント)に更新データを送ります(socket.emit('refresh', data);)。それでは、肝心のNodeChat.reloadの仕組みを見ていきます。

// NodeChat.js

// (中略)

// 現在のチャットデータすべてを取得します。
NodeChat.prototype.reload = function(input)
{
  var cliObj = this.client;
  return new Promise(function(resolve, reject)
  {
    var args = {
      'parameters': {
        'compact': true
        , 'start': input.start
        , 'count': input.count
      }
    };
    cliObj.get(
      'http://localhost/NodeChat.nsf/api/data/collections/name/($All)'
      , args
      , function(retData, response)
      {
        if (retData.code && retData.code !== 200)
          reject(retData);
        else
        {
          resolve({
            items: retData
            , range: response.headers['content-range']
          });
        }
      }
    ).on('error', reject);
  });
}

module.exports = NodeChat;

チャットデータのすべてを取得する方法はいくつか考えられますが、ここでは、Dominoデータサービスのビューエントリーをすべて取得する方法を考えます。このAPIについては以下のリンクに詳細があります。

IBM Notes and Domino Application Development wiki : IBM Domino Access Services 9.0.1

チャットデータのリロードを要求する際に、「start=0、count=countUnit(=10)」という引数を付けて呼び出しましたが、その指定値が変数「args.parameters.start」と「args.parameters.count」に反映されます。ビューから取得するエントリーの範囲を指定しているわけです。この値を持たせて、データベース「NodeChat.nsf」、ビュー「($All)」にHTTP GETを要求すると、指定したビューから指定した範囲のエントリーデータを取得できるわけです。

また、このGETリクエストのレスポンスには、「このデータは○件中○番目から○件のデータです」という情報が、ヘッダー情報の「content-range」に保存されているので、このデータもリスエスト元に返すことで、次の取得範囲を計算できるようにしてあげます。

ここでようやく「public/javascripts/chat.js」の「socket.on('refresh',~」の説明になります。

最初にしていることは、返ってきたデータのうち、rangeプロパティに含まれる件数情報を取得することです。件数情報を正規表現で分解し、start(開始インデックス)、end(終了インデックス)、allCount(全件数)に分けます。startが0ならば、初回のリクエストと言うことで、画面の出力データをクリーンアップします。2回目以降はあるデータの継ぎ足しになるわけです。

次はメッセージ情報を画面に展開していきます。「data.items」に配列としてメッセージデータが含まれているので、これを順次処理していきます。今回はビューデータの列番号「$120」のデータを元に、メッセージとその作成者を特定し、画面に展開しています。

最後に、今回の取得範囲がまだ最後でない場合、取得範囲を変えて、再度「reload」リクエストをWebSocketサーバに要求します。これを繰り返すことで、1回の要求範囲では完全ではない場合でも、非同期処理でいずれでデータが揃うということになります。

最後に、スタイルシートを一部修正します。

// public/stylesheets/style.css

// (中略)

div#chat-window {
  height: 270px;
  background-color: #dddddd;
  overflow: scroll;
}

// (中略)

「overflow: scroll;」を追加しました。これを追加しておかないと、チャットメッセージが増えたときに表示できなくなります。

以上で、約1ヶ月の短期連載は終了です。

DominoとNode.jsの同居サーバ構築 - Chiburu Systemsのブログ

Dominoアクセスになんちゃって認証を追加する - Chiburu Systemsのブログ

Dominoを離れてNode.jsだけでチャットの実装をしてみる - Chiburu Systemsのブログ

Node.jsのチャットデータをNSFに保存してみる - Chiburu Systemsのブログ

Notesで書いたメッセージをチャットに送る - Chiburu Systemsのブログ

この連載の間に、Notes/Dominoのバージョン10に関するニュースも飛び込んできました。Notes/Dominoはまだまだ進化の余地があるテクノロジーです。内側(メーカー)と外側(ベンダー、サードパーティー)で創造と想像を繰り返して、新しい価値を見いだしていけるといいですね。

Notesで書いたメッセージをチャットに送る

前回までに、DominoとNode.jsを同居させ、Node.js上でやり取りされたメッセージをNotesに保存するところまで紹介しました。

今回は、Notesクライアントからのメッセージもチャット上に表示します。最終的には保存されているメッセージをリロードできるようにします。

Notesクライアントで手っ取り早くHTTP送信するには、フォームのPostSaveイベントが使えます。ただし、その場合にはNotesクライアントからDomino+Node.jsへの経路に壁がないことが条件になります。エージェントの保存・更新イベントならその心配はありませんが、今度はリアルタイム性が失われてしまいます。今回はPostSaveイベント方式を使いますが、リアルタイム性があり、通信経路に邪魔が入らなければ、手段は問いません。

Sub Postsave(Source As Notesuidocument)
    Dim xhr As Variant
    Dim session As New NotesSession
    Dim userName As NotesName

    Set userName = New NotesName(session.UserName)

    Set xhr = CreateObject("MSXML2.XMLHTTP")
    xhr.Open "POST", "http://localhost:3000/chat", True
    xhr.SetRequestHeader "Content-Type", "application/json; charset=utf-8"
    xhr.Send |{
        "username": "| & userName.Abbreviated & |",
        "message": "| & Replace(Source.FieldGetText("Subject"), |"|, |\"|) & |"
    }|
End Sub

HTTP通信にはWindowsActiveX経由でMSXML2.XMLHTTPを利用します。送信先localhostですが、Node.jsにたどり着く適切なホスト名を指定します。メッセージの取得元は「Subject」フィールドですが、これは、今回チャットに流用しているディスカッションの仕様によるものなので、データベースとフォームに応じて変更します。

送信側は以上です。次は受信側です。

// routes/chat.js

var express = require('express');
var io;

module.exports = {
  setIo: function(socket_io)
  {
    io = socket_io;
  },
  router: function()
  {
    var router = express.Router();

    /* GET home page. */
    router.post('/', function(req, res)
    {
      io.sockets.emit('chat', req.body);
      res.send('OK');
    });
    return router;
  }
};

新しいルーティングモジュール、routes/chat.jsです。/chatでPOSTされたときに、チャットメンバーにメッセージが送られるようになっています。WebSocketのモジュールを使用するため、setIo関数でモジュールを設定できるようにしています。

今回の主旨に関するソースコードは以上ですが、ルーティングとWebSocketモジュールとの兼ね合いから、他のソースコードの変更を余儀なくされたので、念のため紹介しておきます。

// mySocket.js

var Socket = require('socket.io');
var cookie = require('cookie');
var cookieParser = require('cookie-parser');
var NodeChat = require('./NodeChat');
var io;

module.exports = function(server, sessionStore, User)
{
  io = new Socket(server);

// (中略)

  return io;
};

mySocket.jsでは、初期化されたioを呼び出し元に返すようにしました。これでroutes/chat.js側でWebSocketが使えるようになります。

// app.js

var express = require('express');

// (中略)

// Webアプリケーションサーバー
var app = express();

/**
 * @brief Webアプリケーションサーバーを初期化します。
 * @param session セッション
 * @param sessionStore セッション保管
 * @param passport Passport
 * @param chat Notesからのチャットを受けるルーティング
 */
module.exports = function(session, sessionStore, passport, chat)
{
// (中略)

  // ルーティングを追加します。
  app.use('/', index);
  app.use('/dashboard', dashboard);
  app.use('/login', login(passport));
  app.get('/logout', function(req, res)
  {
    req.logout();
    res.redirect('/');
  });
  app.use('/chat', chat);

// (中略)

  return app;
};

app.jsではさらに引数を増やし、routes/chat.jsが返したルーティングモジュールを引数で受け取り、ルーティングに組み込みます。

// bin/www

// (中略)

/**
 * Webアプリケーションサーバーの初期設定をします。
 */
var chat = require('../routes/chat');
var app = require('../app')(session, sessionStore, auth.passport, chat.router());

var debug = require('debug')('domtest2:server');
var http = require('http');

// (中略)

var server = http.createServer(app);

/**
 * ソケットの初期設定をします。
 */
var io = require('../mySocket')(server, sessionStore, dbInfo.User);
chat.setIo(io);

// (中略)

bin/wwwでは、routes/chat.jsを初期化して、ルーティングへの組み込みとWebSocketの組み込みを行います。app(Webアプリケーション)、server(Webサーバ)、io(WebSocket)の初期化順だと解釈しています。

以上の変更で、以下のような実行結果が得られます。

f:id:takahide-kondoh:20171029142745p:plain

kai shidenさんが「こんにちは」と書いたチャットは前回までと同様です。NotesクライアントからHayato Kobayashiさんが「こんにちは、カイさん。」と返した直後にチャットボードにメッセージが表示されます。通常のWebクライアントであれば、ここはリロードするか、ポーリングするしかないところですね。

次回はいよいよ最終回です。ここまでの仕様では、ブラウザをリロードすると、チャットメッセージは消えてしまいます。リロードしたときに、メッセージ履歴もリロードできるようにして、この連載を終えたいと思います。それでは。