xor

二兎を得るか、一兎をも得ざるか

CRUD別 gun.js チートシート

分散型グラフデータベース「gun.js」を使って試行錯誤したところをまとめました。

github.com

わかったことをメモ的に書いていっているので逐次更新しています。

公式ドキュメントはこちら(英語)
gun.eco



前準備

メインライブラリだけでなく拡張も一緒にrequireしておくべし

後々出てきますが、拡張APIに指定されている notunset メソッドは普通に使用頻度が高い気がするのでまとめてインクルードしたほうがいいです。

<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gun/lib/not.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gun/lib/unset.js"></script>

Nuxtの場合は nuxt.config.js にこちらを。

export default {
  head: {
    script: [
      { src: 'https://cdn.jsdelivr.net/npm/gun/gun.js' },
      { src: 'https://cdn.jsdelivr.net/npm/gun/lib/not.js' },
      { src: 'https://cdn.jsdelivr.net/npm/gun/lib/unset.js' },
    ],
  },
}

VueあるいはNuxtで使う時はノード変数をVuexにストアしない

gunオブジェクトをVuexで共有するのは避けた方がよさそうです。
getメソッドも reactivegetter のためか副作用があり、Vuexゲッター内で使うと「値の変更がある」とみなされてしまうためです。

export const state = () => ({
    gun: null,
});

export const getters = {
    gunNode: (state) => (key) => {
        return state.gun.get(key);
    },
};

Vuexを上記のようにして、コンポーネントthis.$store.getters['gunNode']('list') をすると、

Error: [vuex] do not mutate vuex store state outside mutation handlers.

このようなエラーが発生します。
gunオブジェクト自体もリアクティブなので、Vuexとは別のストアだと考えたほうがよいです。

Create

グラフデータの作成(ノードの接続)

新たにデータを作成、さらにそのデータをいずれかのノードの下にぶら下げたい場合。
例として、全てのデータはルート直下の data ノードの下に作成されるが、そのデータをカテゴリ別のノードに所属させたいとき、データのputが確実に終わってからsetを行わなければいけない。
公式のAPIではPromiseがまだexperimentalとなっており、コールバックを基本としたメソッドのみしか提供されていないため、Promiseのコンストラクタで囲ってチェインさせるのがすっきりする。

const gun = Gun();
const _id = 'me0001';
const alldata = 'animals'
const category = 'cat';

(new Promise((resolve, reject) => {
  // まずデータをデータノードの直下に作る
  const target_node = this.gun.get(alldata).get(_id);
  data_node.put({
    id: _id,
    date: (new Date()).toLocaleString(),
    name: 'たま',
    age: 4,
    sex: 'female',
  }, ack => {
    if (ack.error) reject(ack.error);
    else resolve(data_node);
  });
})).then(target_node => {
  // カテゴリノードの直下にいれる
  return new Promise((resolve, reject) => {
    this.gun.get(category).set(target_node, ack => {
      if (ack.error) reject(ack.error);
      else resolve();
    });
  });
}).then(() => {
  // 成功
  alert('正常に登録されました');
}).catch(err => {
  // 失敗
  console.error('Error:', err);
  alert('エラーが発生しました');
});

配列の代わりにグラフ接続 .set を利用する

配列を含むデータを登録しようとすると、コールバックは正常値を返しデータ全体も確かに登録されますが、対象の配列とそのキーだけごっそり抜け落ちてしまいます。

const gun = Gun();

const data = {
  name: 'dave',
  children: [ 'alice', 'bob', 'charlie' ],
};

const parent_node = gun.get('parent').put(data);
// エラーではないが以下のようなログが出る
// Invalid value at 'parent.children'! Use `.set(item)` instead of an Array.

const registered = gun.get('parent').get('children'); // 何も入っていない

配列はそのまま登録せず、別々に登録したデータノードをどちらかにぶら下げるようにして、1方向のグラフを形成させるようにします。

const gun = Gun();

const alice_node = gun.get('alice').put('alice');
const bob_node = gun.get('bob').put('bob');
const charlie_node = gun.get('charlie').put('charlie');

const data = { name: 'dave' };

const parent_node = gun.get('parent').put(data);

// グラフを生成する
parent_node.get('children').set(alice_node);
parent_node.get('children').set(bob_node);
parent_node.get('children').set(charlie_node);

Read

実際のデータはコールバック内で取得する

get メソッドによって取得できるのはgunノードオブジェクトです。
実際に中に格納したデータを取得するにはコールバックを利用します。

const gun = Gun();
const put_node = gun.get('message').('sent').put('hello');

const get_node = gun.get('message');
console.log(get_node.get('sent')); // これはGunオブジェクト

get_node.get('sent', ack => {
  console.log(ack.err); // 取得できなかったとき
  console.log(ack.put); // 'hello' (値)
  console.log(ack.get); // 'sent' (キー)
});

// 公式は一般には以下を推奨とのこと
get_node.get('sent').on((val, key) => {
  console.log(val); // 'hello' (値)
  console.log(key); // 'sent' (キー)
});

データが存在する場合としない場合で区別する

指定したキーのノードにデータが入っていない場合は、コールバックは実行されません。
そのため not メソッドによって格納データの有無を確認して場合分けしたほうがよさそうです。

const gun = Gun();
const id = 'stack0001';

gun.get('list').get(id).not(key => {
  // 何も登録されていない
  console.log(key, 'データが存在しません');
}).on(data => {
  // 何かしらデータがある
  console.log(data);
});

上記のようにしてデータを取得したあとで、後述する「null 代入によるデータの削除」を行うと .on が再度発火してしまうので、そう言った場合には .once を使ったほうがよいです。

Delete

データ削除

ノードに対して null をputすれば、ガベージコレクション的なのが走ったときにそのノードのデータは解放されるので問題ないらしいです。

const gun = Gun();
const data_node = gun.get('data');

// データの入力
const entry = data_node.get('chicken').put({
  type: 'ukokkei',
  age_of_the_day: 45,
});
// put() はメソッドチェインに副作用を及ぼさない

// データの削除
data_node.get('chicken').put(null);
// または
entry.put(null);

// ルート直下のノードに含まれるデータはObjectである必要がある
data_node.put(null);
// エラーではないが、gun.js内で次のログが発出される
// Data saved to the root level of the graph must be a node (an object), not a object of "null"!

// これはおそらくOK
data_node.put({});