Derby のモデルは Racer によって動作している。これはリアルタイムなモデル同期エンジンだ。 Racer は洗練されたコンフリクト検出・解決アルゴリズムを通じて、複数ユーザが同じデータを扱うことを可能にしている。それと同時に、シンプルなオブジェクトアクセッサとイベントインターフェースを、アプリケーションロジック構築のために提供している。
サーバ上で Racer はストア store
を作る。これはデータの更新を管理する。ストア上の非同期なメソッドを通じて、直接にデータを操作することが可能であり、それはデータベースとのやりとりと似たものである。ストアはまたモデルオブジェクトを作り、これはむしろオブジェクトに対する直接のやりとりに似た、同期的なインターフェースを提供する。
モデルはそれ自身でグローバル状態のサブセットのコピーを保持する。このサブセットは、あるパスへの署名 subscription
を通じて定義される。モデルは独立して実行され、 Socket.IO を通して自動的にその状態を関連付けられたストアに同期させる。
モデルに変更があったとき、モデルは即座に変更を反映する。バックグラウンドで Racer は動作をトランザクションに切り替えるが、そのトランザクションはサーバへと送られ、適用もしくは拒絶される。もしトランザクションが受け入れられたなら、それは同じデータに署名しているその他全てのクライアントへと送られる。この楽観的アプローチはユーザに対する即応的な作用を提供し、アプリケーションロジックの構築を容易に、また Racer がオフラインで動作することを可能にする。
モデルの mutator メソッドは、トランザクション成功もしくは失敗後のコールバックを提供する。これらのコールバックはアプリケーションに固有な、コンフリクト解決UIを提供するために使える。モデルはまた、コンテンツが更新されたときにイベントを発行する。これは Derby がリアルタイムにビューを更新するために使われている。
現在、 Racer はデフォルトで、命令を受けた時に全てトランザクションが適用されるようになっている。すなわち、 'last-writer-wins' だ。リアルタイムに接続されたクライアントに対しては、これは一般的に予想された通りのふるまいをする。しかし、同じデータに関わるオフラインユーザは、コンフリクトした更新を産んでしまうことがあり、これは予期しないふるまいを導きうる。
そのためRacer は、 Software Transactional Memory (STM) Operational Transformation (OT) Diff-match-patch の複合的なテクニックを用いてコンフリクトの解決を支援している。
これらの特徴はまだ完全には実装されていないが、 Racer デモが STM と OT の準備的な実例を示している。Letters は STM モードを使い、異なるユーザが同時に同じ文字を移動させた場合、そのコンフリクトを自動的に検知する。Pad は OT を最小限のテキストエディタのために使う。
これらのアルゴリズムを動作させるため、 Racer は全てのトランザクションの履歴を保持している。新しいトランザクションが届いた時、それらのパスとバージョンは、すでに履歴にコミットされたトランザクションと比較される。 STM は、同じパス上の他のオペレーションとコンフリクトがない場合に、トランザクションを承認する。 STM は
ry
Derby project generator によって作られたデフォルトのサーバは、ストアを作り、アプリケーションルーターより先に modelMiddleware を Express サーバに付け加える。 modelMiddleware は reg.getModel()
関数を加える。これはモデルを作る場合、あるいはリクエストに対して(もしすでに作られているなら)それを取得する場合に呼び出すことができる。
store = derby.createStore ( options )
options: An object that configures the store
store: Returns a Racer store object
(略)
(略)
Derby はアプリケーションルートが呼ばれた時に、モデルを提供する。サーバでは、アプリケーションに関連付けられた store
をもとに空のモデルが作られる。サーバがページを描画した際、モデルはシリアライズされる。そして、クライアント上のものと同じ状態へと再初期化される。モデルオブジェクトは app.ready()
コールバックと、クライアント上で描画されたアプリケーションルートに渡される。
Derby は req.getModel()
を呼ぶことで store.modelMiddleware から提供されるモデルを使う。サーバサイドの express ミドルウェアもしくはルートからデータを渡すため、モデルは同じメソッドを用いて回収することが可能であり、データはアプリケーションルーターに制御が移る前に、それにセットされることができる。???
model = store.createModel ( )
model: A Racer model object associated with the given store
サーバサイドのルートは、 req.getModel()
によってモデルを使うことができる。他には、 store.createModel()
によってモデルを手動で作ることも可能である。
全てのモデルの動作は、パス Paths
の上で発生する。パスはネストしたリテラルオブジェクトとして表される。これらのパスは、特定のストアのなかでユニークなものでなければならない。
例えば、以下のモデルは
{
title: 'Fruit store',
fruits: [
{ name: 'banana', color: 'yellow' },
{ name: 'apple', color: 'red' },
{ name: 'lime', color: 'green' }
]
}
title
, fruits.1
, fruits.0.color
というパスを持つ。パスは JavaScript 変数名として適切なもの――英数字とアンダースコア _
から成り、文字かアンダースコアで始まるもの――、あるいはドット .
で連結された配列のインデックスで構成される。それらにドル記号 $
を含めることは勧められない。それは内部的な用途のために予約されているからである。
アンダースコアで始まる部分を持つパス(_showFooter
や flowers.10._hovered
など)は、特別な意味を持つ。これらのパスは "private" なものであり、サーバや他のクライアントに対して同期されない。プライベートパスは、参照 reference
や描画目的のために頻繁に使われる。
(略)
モデルミューテーターメソッドは、楽観的に作用する。つまり、変更は即座に反映されるが、いつかは失敗しロールバックされる。全てのモデルミューテーターメソッドは同期的であり、オプショナルなコールバックを提供する。
これらのメソッドは、モデルのパスを指定することで、あるオブジェクトを取得、代入、削除するために使うことができる。
value = model.get ( [path] )
path: (optional) Path of object to get. Not supplying a path will return all data in the model
value: Current value of the object at the given path. Note that objects are returned by reference and should not be modified directly
すべてのモデルミューテーターは、オプショナルなコールバックをもっており、 callback(err, methodArgs...)
のように引数を指定する。トランザクションが成功すれば、 err
は null
である。そうでない場合は、これはエラーメッセージの文字列となる。他のトランザクションとコンフリクトしている場合、メッセージは conflict
となる。当初関数を呼ぶときに使われた引数(つまり、 model.set()
に対するpath, value
)は、コールバックに対しても渡される。
(略)
モデルは標準的な Node.js の EventEmitter を継承しており、それと同じ on
, once
, removeListener
, emit
等のメソッドをサポートする。
Racer は、model.set
や model.push
等によってデータが変化させられた時は、常にイベントを発行する。これらのイベントはアプリケーションに対してエントリーポイントを提供し、アプリケーションはそれによって特定のデータ操作やデータ操作のパターンに対して反応を起こす。
`model.onと
model.once` はこれらのタイプのイベントのために、第二引数を設けている。第二匹数は、パスのパターンか正規表現であり、これが発行されたイベントをフィルターしたり、ミューテータがパターンに合致したときにのみハンドラ関数を読んだりする。
listener = model.on ( method, path, eventCallback )
method: Name of the mutator method - e.g., “set”, “push”
path: Pattern or regular expression matching the path being mutated
eventCallback: Function to call when a matching method and path are mutated
listener: Returns the listener function subscribed to the event emitter. This is the function that should be passed to model.removeListener
イベントコールバックはパスのパターンとメソッドに基づく沢山の引数を受ける。引数は次のようになる。
eventCallback ( captures..., args..., out, isLocal, passed )
captures: The capturing groups from the path pattern or regular expression. If specifying a string pattern, a capturing group will be created for each * wildcard and anything in parentheses, such as (one|two)
args: The arguments to the method. Note that optional arguments with a default value (such as the byNum argument of model.incr) will always be included
out: The return value of the model mutator method
isLocal: true if the model mutation was originally called on the same model and false otherwise
passed: undefined, unless a value is specified via model.pass. See description below
パスのパターンでは、ワイルドカード *
がパスの中間にある場合、中間の一つの部分にのみマッチするが、終端にある場合はひとつまたは複数のパスの部分にマッチする。言い換えれば、パターンの中間にある場合は non-greedy だが、終端にある場合は greedy である。
// Matches only model.push('messages', message)
model.on('push', 'messages', function (message, messagesLength) {
...
});
// Matches model.set('todos.4.completed', true), etc.
model.on('set', 'todos.*.completed', function (todoId, isComplete) {
...
});
// Matches all set operations
model.on('set', '*', function (path, value) {
...
});
(略)
model.subscribe
メソッドは、関連付けられたストアからデータのあるモデルを追加し、このデータが変更に伴った最新の状態を維持すべきことを宣言する。サブスクリプションは、パスパターンやクエリによって定義することも可能である。
一般的には、サブスクリプションはページを描画する前の、ルートにレスポンスを行う時点で行われる。しかしサブスクリプションのためのメソッドは、サーバやブラウザ上のどのコンテクストでも呼び出せる。サーバ側でページが描画される以前に行われた全てのサブスクリプションは、ブラウザ上でページがもう一度読み込まれるときに、再び行われる。
model.subscribe ( targets..., [callback] )
targets: One or more path patterns or queries to subscribe to
callback: (optional) Called after subscription succeeds and the data is set in the model or upon an error
サブスクリプションのコールバックは、 callback(err, scopedModels...)
という引数をとる。トランザクションが成功した場合、 err
は null
である。そうでない場合は、ここにはエラーメッセージの文字列が入る。もし Socket.IO がその時点で繋がっていないときは、メッセージは disconnected
である。残りの引数はスコープドモデルであり、これは個々のサブスクリプションの対象パスと一致する。
モデルがすでに対象へのサブスクリプションを有している場合、その同じ対象に対してサブスクリプションをもう一度行う事は、特に何の効果も産まない。すべての対象がすでにサブスクリプションの対象であった場合は、コールバックは即座に呼ばれる。
(略)
パスのパターンは文字列として指定し、それがモデルのパスに対応する。パスのパターンはオブジェクト全体に対してサブスクリプションを行い、それには全てのサブパスも含まれる。例えば、 rooms.lobby
に対してサブスクリプションを行う場合、そのパスの下位の全てのデータ、 rooms.lobby.name
や rooms.lobby.items.3.location
に対してもサブスクリプションを行うことになる。
(略)
参照は一般的な方法で、モデルに作用するビジネスロジックやテンプレートを構築することを可能にする。それは基礎的なデータに対する参照パスからモデルオペレーションを転送し、イベントリスナーを設定する。これは、正確なオブジェクトのパスとその参照の両方のモデルイベントを起こす。
参照はモデルごとに宣言されなければならない。 model.ref
を呼ぶと、モデルの中の参照オブジェクトの設定に加えて、たくさんのイベントリスナーを作り出す。参照が作られた時、 set
モデルイベントが発生する。内部的には、 model.set
はモデルにリファレンスを追加するために使われている。
fn = model.ref ( path, to, [key] )
path: The location at which to create a reference. This must be a private path, since references must be declared per model
to: The location that the reference links to. This is where the data is actually stored. May be a path or scoped model
key: (optional) A path whose value should be added as an additional property underneath to when accessing the reference. May be a path or scoped model
fn: Returns the function that is stored in the model to represent the reference. This function should not be used directly
model.set('colors', {
red: {hex: '#f00'}
, green: {hex: '#0f0'}
, blue: {hex: '#00f'}
});
// Getting a reference returns the referenced data
model.ref('_green', 'colors.green');
// Logs {hex: '#0f0'}
console.log(model.get('_green'));
// Setting a property of the reference path modifies
// the underlying data
model.set('_green.rgb', [0, 255, 0]);
// Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log(model.get('colors.green'));
// Setting or deleting the reference path modifies
// the reference and not the underlying data
model.del('_green');
// Logs undefined
console.log(model.get('_green'));
// Logs {hex: '#0f0', rgb: [0, 255, 0]}
console.log(model.get('colors.green'));
// Changing a reference key updates the reference
model.set('selected', 'red');
model.ref('_selectedColor', 'colors', 'selected');
// Logs '#f00'
console.log(model.get('_selectedColor.hex'));
model.set('selected', 'blue');
// Logs '#00f'
console.log(model.get('_selectedColor.hex'));
(略)