Aggregation
Aggregation は、大勢の書き手が一つの名前の下に書いたものを、どの読み手にも同じ一つの文書にまとめ、各書き手に変更ごとに適用されたかを伝え、全書き手の今の値を一覧します。
パス
パスは参加者のルートからの相対です。{name} は空を含む任意のパスで、{writer} は最初の .aggregation の段より後ろのすべてです。
| 書く人 | パス | トラック | 中身 |
|---|---|---|---|
| 書き手 | {name}/.aggregation/{writer} |
patch.json |
書き手の変更: 各グループに一つのフレーム、RFC 7396 の merge patch として当てる JSON オブジェクト。グループの番号が変更の番号 |
| 書き手 | 同じ | value.json |
書き手の今の値。JSON スナップショットのトラック |
| Aggregation | .aggregation/{name} |
state.json |
まとめた文書。JSON スナップショットのトラック |
| Aggregation | 同じ | writers.json |
{"{writer}": value} の JSON スナップショットのトラック。今 value.json を出している書き手ごとに一項目 |
| Aggregation | 同じ | applied/{writer} |
その書き手の変更ごとに一フレーム: {"applied": n} か {"refused": n, "code": name} |
alice として書けるのは、publish が {name}/.aggregation/alice を覆うセッションだけです。そのため publish が board/.aggregation/{id} のリンクなら、各自は自分としてボードに書けます。.aggregation/{name} を読むと書き手が書いたものが分かるので、書き手のパスと同じように許してください。
// A shared board through Aggregation: changes go to board/.aggregation/{me}, the merged board and
// every writer's current value come from .aggregation/board.
import { type Connection, json, moq } from 'tablebox.io';
export async function board(connection: Connection, me: string, show: (board: unknown, writers: unknown) => void) {
const mine = connection.publish(`board/.aggregation/${me}`);
const changes = mine.createTrack('patch.json');
const value = new json.Snapshot.Producer({ track: mine.createTrack('value.json') });
// A writer never uses a change number twice, so each broadcast numbers its changes from the time.
let next = Date.now();
const merged = await connection.read('.aggregation/board');
const state = new json.Snapshot.Consumer({ track: merged.track('state.json').subscribe() });
const writers = new json.Snapshot.Consumer({ track: merged.track('writers.json').subscribe() });
let board: unknown;
let everyone: unknown;
void (async () => { for await (board of state) show(board, everyone); })();
void (async () => { for await (everyone of writers) show(board, everyone); })();
return {
// One RFC 7396 merge patch per change: { "note-1": { "text": "Hi" } } sets a key, null deletes it.
change: (patch: Record<string, unknown>) => {
const group = new moq.Group.Producer(next++);
changes.writeGroup(group);
group.writeJson(patch);
group.close();
},
// This writer's own entry in writers.json, such as its name and cursor.
set: (mine: unknown) => value.update(mine),
};
}まとめ方と確認
.aggregation/{name} のどれかのトラックを読むと、その名前の Aggregation が動き始め、{name}/.aggregation/ の下の書き手をすべて読みます。各書き手の変更はその書き手の順に、異なる書き手の変更は Aggregation に届いた順に当て、どのキーでも最後に当てた変更が勝ちます。state.json の読み手は、どの Relay にいても、同じ文書の並びを読みます。
Aggregation が動いている間に書き手が出した変更には、applied/{writer} に一フレームずつ返ります。state.json に入ったら applied、そうでなければ refused と、bad-request(フレームが JSON オブジェクトでない)、too-large(文書が 16 MiB を超える)、missing-variable のどれかです。n はその変更のグループ番号です。書き手は変更に 1 ずつ増える番号を振り、同じ番号を二度使いません。各ブロードキャストは Unix エポックからのミリ秒の時刻から始め、それより前の番号の方が大きければその次から始めます。そのため applied/{writer} の一フレームは、書き手のどのブロードキャストから来た変更でも、一つの変更を指します。書き手は書く前に自分の applied/{writer} を読み、その読みが始まり直すたびに(再接続の後や、引き継ぎで終わった後)、確認の返っていない変更を出し直します。merge patch は二度当てても同じ文書になります。
全書き手の値
writers.json は、名前やカーソルなど、各書き手の最新の value.json の値を持ちます。書き手の項目は、そのブロードキャストか value.json が終わると消えるので、一覧は今いる人に合わせて変わります。全員が全員のブロードキャストを一覧するには大きすぎる範囲では、代わりに人を writers.json に置き、要る人だけが読めます。
保存
state.json は、保存してある文書から始まり、変わるたびに Persistence を通して保存されます。そのため Persistence のストレージの変数が要ります。Aggregation は、Persistence が保存してある文書を渡してから state.json に出し、何も保存されていなければ {} から始めます。文書は、ストレージの STORAGE_PREFIX の下の .aggregation/{project path of name}/state.json にあります。
動き方と失敗
Aggregation は、出力が読まれているか書き手がいる間動き、どちらもいなくなってから 20 秒で止まります。state.json を保存する Persistence は、最後の書き手が去ってから 20 秒までそれを読むので、ほかに読む人のいない名前は、最後の書き手が去ってから 40 秒で止まります。一つのプロジェクトのすべての名前は、一度に一つのインスタンスが受け持ちます。
- 名前が動き始めるときにストレージの変数がない:
state.jsonはmissing-variableで断られ、どの変更も同じく断られます。writers.jsonは動きます。変数を設定してから読み直せば動きます。 - 文書が始まる前に Persistence に届かない、または失敗する:
state.jsonは待ち、変更は答えが来るまで確認されません。 - 文書が始まった後で保存が止まる: 文書は続き、変更は当てられて確認されます。Persistence が保存できるようになれば、また保存されます。
- インスタンスが止まる: 読み出しは終わり、読み直すと引き継いだインスタンスに届きます。そのインスタンスは保存してある文書(最大 10 秒前のもの)から始め、書き手のブロードキャストがまだ持っている変更をもう一度当ててから、受け取った変更を当てます。そうした変更の後でほかの書き手が変えたキーでは、古い値が戻ることがあります。最後の保存より後に確認され、もう持たれていない変更は、書き手が出し直さない限り失われます。
- 書き手が去る: 当てた変更は
state.jsonに残り、その項目はwriters.jsonから消えます。 - インスタンスを足す・減らす: 一つのプロジェクトを二つのインスタンスが一瞬受け持つことがあり、古いインスタンスが手放すまで、違う Relay の読み手には違う文書が見えることがあります。