diff --git a/CHANGELOG.md b/CHANGELOG.md
index c492e43839..b26010b146 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,88 @@ ChangeLog
 
 This document describes breaking changes only.
 
+10.0.0
+------
+
+ストリーミングAPIに破壊的変更があります。運営者がすべきことはありません。
+
+変更は以下の通りです
+
+* ストリーミングでやり取りする際の snake_case が全て camelCase に
+* リバーシのストリームエンドポイント名が reversi → gamesReversi、reversiGame → gamesReversiGame に
+* ストリーミングの個々のエンドポイントが廃止され、一旦元となるストリームに接続してから、個々のチャンネル(今までのエンドポイント)に接続します。詳細は後述します。
+* ストリームから流れてくる、キャプチャした投稿の更新イベントに投稿自体のデータは含まれず、代わりにアクションが設定されるようになります。詳細は後述します。
+* ストリームに接続する際に追加で指定していたパラメータ(トークン除く)が、URLにクエリとして含むのではなくチャンネル接続時にパラメータ指定するように
+
+### 個々のエンドポイントが廃止されることによる新しいストリーミングAPIの利用方法
+具体的には、まず https://example.misskey/streaming にwebsocket接続します。
+次に、例えば「messaging」ストリーム(チャンネルと呼びます)に接続したいときは、ストリームに次のようなデータを送信します:
+``` javascript
+{
+  type: 'connect',
+  body: {
+    channel: 'messaging',
+    id: 'foobar',
+    params: {
+      otherparty: 'xxxxxxxxxxxx'
+    }
+  }
+}
+```
+ここで、`id`にはそのチャンネルとやり取りするための任意のIDを設定します。
+IDはチャンネルごとではなく「チャンネルの接続ごと」です。なぜなら、同じチャンネルに異なるパラメータで複数接続するケースもあるからです。
+`params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。
+
+チャンネルにメッセージを送信するには、次のようなデータを送信します:
+``` javascript
+{
+  type: 'channel',
+  body: {
+    id: 'foobar',
+    type: 'something',
+    body: {
+      some: 'thing'
+    }
+  }
+}
+```
+ここで、`id`にはチャンネルに接続するときに指定したIDを設定します。
+
+逆に、チャンネルからメッセージが流れてくると、次のようなデータが受信されます:
+``` javascript
+{
+  type: 'channel',
+  body: {
+    id: 'foobar',
+    type: 'something',
+    body: {
+      some: 'thing'
+    }
+  }
+}
+```
+ここで、`id`にはチャンネルに接続するときに指定したIDが設定されています。
+
+### 投稿のキャプチャに関する変更
+投稿の更新イベントに投稿情報は含まれなくなりました。代わりに、その投稿が「リアクションされた」「アンケートに投票された」「削除された」といったアクション情報が設定されます。
+
+具体的には次のようなデータが受信されます:
+``` javascript
+{
+  type: 'noteUpdated',
+  body: {
+    id: 'xxxxxxxxxxx',
+    type: 'reacted',
+    body: {
+      reaction: 'hmm'
+    }
+  }
+}
+```
+
+* reacted ... 投稿にリアクションされた。`reaction`プロパティにリアクションコードが含まれます。
+* pollVoted ... アンケートに投票された。`choice`プロパティに選択肢ID、`userId`に投票者IDが含まれます。
+
 9.0.0
 -----
 
diff --git a/package.json b/package.json
index 27bf5c0f13..dc76ad0f81 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,7 @@
 		"@types/websocket": "0.0.40",
 		"@types/ws": "6.0.1",
 		"animejs": "2.2.0",
+		"autobind-decorator": "2.1.0",
 		"autosize": "4.0.2",
 		"autwh": "0.1.0",
 		"bcryptjs": "2.4.3",
@@ -225,8 +226,8 @@
 		"vuex-persistedstate": "2.5.4",
 		"web-push": "3.3.3",
 		"webfinger.js": "2.6.6",
-		"webpack-cli": "3.1.2",
 		"webpack": "4.20.2",
+		"webpack-cli": "3.1.2",
 		"websocket": "1.0.28",
 		"ws": "6.0.0",
 		"xev": "2.0.1"
diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts
index f42af94370..65087cc98e 100644
--- a/src/client/app/common/scripts/compose-notification.ts
+++ b/src/client/app/common/scripts/compose-notification.ts
@@ -13,21 +13,21 @@ type Notification = {
 
 export default function(type, data): Notification {
 	switch (type) {
-		case 'drive_file_created':
+		case 'driveFileCreated':
 			return {
 				title: '%i18n:common.notification.file-uploaded%',
 				body: data.name,
 				icon: data.url
 			};
 
-		case 'unread_messaging_message':
+		case 'unreadMessagingMessage':
 			return {
 				title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] ,
 				body: data.text, // TODO: getMessagingMessageSummary(data),
 				icon: data.user.avatarUrl
 			};
 
-		case 'reversi_invited':
+		case 'reversiInvited':
 			return {
 				title: '%i18n:common.notification.reversi-invited%',
 				body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1],
diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts
new file mode 100644
index 0000000000..5fc82942d5
--- /dev/null
+++ b/src/client/app/common/scripts/note-subscriber.ts
@@ -0,0 +1,105 @@
+import Vue from 'vue';
+
+export default prop => ({
+	data() {
+		return {
+			connection: null
+		};
+	},
+
+	computed: {
+		$_ns_note_(): any {
+			return this[prop];
+		},
+
+		$_ns_isRenote(): boolean {
+			return (this.$_ns_note_.renote &&
+				this.$_ns_note_.text == null &&
+				this.$_ns_note_.fileIds.length == 0 &&
+				this.$_ns_note_.poll == null);
+		},
+
+		$_ns_target(): any {
+			return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_;
+		},
+	},
+
+	created() {
+		if (this.$store.getters.isSignedIn) {
+			this.connection = (this as any).os.stream;
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if (this.$store.getters.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if (this.$store.getters.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if (this.$store.getters.isSignedIn) {
+				const data = {
+					id: this.$_ns_target.id
+				} as any;
+
+				if (
+					(this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) ||
+					(this.$_ns_target.mentions || []).includes(this.$store.state.i.id)
+				) {
+					data.read = true;
+				}
+
+				this.connection.send('sn', data);
+				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		decapture(withHandler = false) {
+			if (this.$store.getters.isSignedIn) {
+				this.connection.send('un', {
+					id: this.$_ns_target.id
+				});
+				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
+			}
+		},
+
+		onStreamConnected() {
+			this.capture();
+		},
+
+		onStreamNoteUpdated(data) {
+			const { type, id, body } = data;
+
+			if (id !== this.$_ns_target.id) return;
+
+			switch (type) {
+				case 'reacted': {
+					const reaction = body.reaction;
+					if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {});
+					this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1;
+					break;
+				}
+
+				case 'pollVoted': {
+					if (body.userId == this.$store.state.i.id) return;
+					const choice = body.choice;
+					this.$_ns_target.poll.choices.find(c => c.id === choice).votes++;
+					break;
+				}
+			}
+
+			this.$emit(`update:${prop}`, this.$_ns_note_);
+		},
+	}
+});
diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts
new file mode 100644
index 0000000000..7dc130937b
--- /dev/null
+++ b/src/client/app/common/scripts/stream.ts
@@ -0,0 +1,318 @@
+import autobind from 'autobind-decorator';
+import { EventEmitter } from 'eventemitter3';
+import * as ReconnectingWebsocket from 'reconnecting-websocket';
+import { wsUrl } from '../../config';
+import MiOS from '../../mios';
+
+/**
+ * Misskey stream connection
+ */
+export default class Stream extends EventEmitter {
+	private stream: ReconnectingWebsocket;
+	private state: string;
+	private buffer: any[];
+	private sharedConnections: SharedConnection[] = [];
+	private nonSharedConnections: NonSharedConnection[] = [];
+
+	constructor(os: MiOS) {
+		super();
+
+		this.state = 'initializing';
+		this.buffer = [];
+
+		const user = os.store.state.i;
+
+		this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : ''));
+		this.stream.addEventListener('open', this.onOpen);
+		this.stream.addEventListener('close', this.onClose);
+		this.stream.addEventListener('message', this.onMessage);
+
+		if (user) {
+			const main = this.useSharedConnection('main');
+
+			// 自分の情報が更新されたとき
+			main.on('meUpdated', i => {
+				os.store.dispatch('mergeMe', i);
+			});
+
+			main.on('readAllNotifications', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadNotification: false
+				});
+			});
+
+			main.on('unreadNotification', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadNotification: true
+				});
+			});
+
+			main.on('readAllMessagingMessages', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadMessagingMessage: false
+				});
+			});
+
+			main.on('unreadMessagingMessage', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadMessagingMessage: true
+				});
+			});
+
+			main.on('unreadMention', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadMentions: true
+				});
+			});
+
+			main.on('readAllUnreadMentions', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadMentions: false
+				});
+			});
+
+			main.on('unreadSpecifiedNote', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadSpecifiedNotes: true
+				});
+			});
+
+			main.on('readAllUnreadSpecifiedNotes', () => {
+				os.store.dispatch('mergeMe', {
+					hasUnreadSpecifiedNotes: false
+				});
+			});
+
+			main.on('clientSettingUpdated', x => {
+				os.store.commit('settings/set', {
+					key: x.key,
+					value: x.value
+				});
+			});
+
+			main.on('homeUpdated', x => {
+				os.store.commit('settings/setHome', x);
+			});
+
+			main.on('mobileHomeUpdated', x => {
+				os.store.commit('settings/setMobileHome', x);
+			});
+
+			main.on('widgetUpdated', x => {
+				os.store.commit('settings/setWidget', {
+					id: x.id,
+					data: x.data
+				});
+			});
+
+			// トークンが再生成されたとき
+			// このままではMisskeyが利用できないので強制的にサインアウトさせる
+			main.on('myTokenRegenerated', () => {
+				alert('%i18n:common.my-token-regenerated%');
+				os.signout();
+			});
+		}
+	}
+
+	public useSharedConnection = (channel: string): SharedConnection => {
+		const existConnection = this.sharedConnections.find(c => c.channel === channel);
+
+		if (existConnection) {
+			existConnection.use();
+			return existConnection;
+		} else {
+			const connection = new SharedConnection(this, channel);
+			connection.use();
+			this.sharedConnections.push(connection);
+			return connection;
+		}
+	}
+
+	@autobind
+	public removeSharedConnection(connection: SharedConnection) {
+		this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id);
+	}
+
+	public connectToChannel = (channel: string, params?: any): NonSharedConnection => {
+		const connection = new NonSharedConnection(this, channel, params);
+		this.nonSharedConnections.push(connection);
+		return connection;
+	}
+
+	@autobind
+	public disconnectToChannel(connection: NonSharedConnection) {
+		this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id);
+	}
+
+	/**
+	 * Callback of when open connection
+	 */
+	@autobind
+	private onOpen() {
+		const isReconnect = this.state == 'reconnecting';
+
+		this.state = 'connected';
+		this.emit('_connected_');
+
+		// バッファーを処理
+		const _buffer = [].concat(this.buffer); // Shallow copy
+		this.buffer = []; // Clear buffer
+		_buffer.forEach(data => {
+			this.send(data); // Resend each buffered messages
+		});
+
+		// チャンネル再接続
+		if (isReconnect) {
+			this.sharedConnections.forEach(c => {
+				c.connect();
+			});
+			this.nonSharedConnections.forEach(c => {
+				c.connect();
+			});
+		}
+	}
+
+	/**
+	 * Callback of when close connection
+	 */
+	@autobind
+	private onClose() {
+		this.state = 'reconnecting';
+		this.emit('_disconnected_');
+	}
+
+	/**
+	 * Callback of when received a message from connection
+	 */
+	@autobind
+	private onMessage(message) {
+		const { type, body } = JSON.parse(message.data);
+
+		if (type == 'channel') {
+			const id = body.id;
+			const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id);
+			connection.emit(body.type, body.body);
+		} else {
+			this.emit(type, body);
+		}
+	}
+
+	/**
+	 * Send a message to connection
+	 */
+	@autobind
+	public send(typeOrPayload, payload?) {
+		const data = payload === undefined ? typeOrPayload : {
+			type: typeOrPayload,
+			body: payload
+		};
+
+		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
+		if (this.state != 'connected') {
+			this.buffer.push(data);
+			return;
+		}
+
+		this.stream.send(JSON.stringify(data));
+	}
+
+	/**
+	 * Close this connection
+	 */
+	@autobind
+	public close() {
+		this.stream.removeEventListener('open', this.onOpen);
+		this.stream.removeEventListener('message', this.onMessage);
+	}
+}
+
+abstract class Connection extends EventEmitter {
+	public channel: string;
+	public id: string;
+	protected params: any;
+	protected stream: Stream;
+
+	constructor(stream: Stream, channel: string, params?: any) {
+		super();
+
+		this.stream = stream;
+		this.channel = channel;
+		this.params = params;
+		this.id = Math.random().toString();
+		this.connect();
+	}
+
+	@autobind
+	public connect() {
+		this.stream.send('connect', {
+			channel: this.channel,
+			id: this.id,
+			params: this.params
+		});
+	}
+
+	@autobind
+	public send(typeOrPayload, payload?) {
+		const data = payload === undefined ? typeOrPayload : {
+			type: typeOrPayload,
+			body: payload
+		};
+
+		this.stream.send('channel', {
+			id: this.id,
+			body: data
+		});
+	}
+
+	public abstract dispose: () => void;
+}
+
+class SharedConnection extends Connection {
+	private users = 0;
+	private disposeTimerId: any;
+
+	constructor(stream: Stream, channel: string) {
+		super(stream, channel);
+	}
+
+	@autobind
+	public use() {
+		this.users++;
+
+		// タイマー解除
+		if (this.disposeTimerId) {
+			clearTimeout(this.disposeTimerId);
+			this.disposeTimerId = null;
+		}
+	}
+
+	@autobind
+	public dispose() {
+		this.users--;
+
+		// そのコネクションの利用者が誰もいなくなったら
+		if (this.users === 0) {
+			// また直ぐに再利用される可能性があるので、一定時間待ち、
+			// 新たな利用者が現れなければコネクションを切断する
+			this.disposeTimerId = setTimeout(() => {
+				this.disposeTimerId = null;
+				this.removeAllListeners();
+				this.stream.send('disconnect', { id: this.id });
+				this.stream.removeSharedConnection(this);
+			}, 3000);
+		}
+	}
+}
+
+class NonSharedConnection extends Connection {
+	constructor(stream: Stream, channel: string, params?: any) {
+		super(stream, channel, params);
+	}
+
+	@autobind
+	public dispose() {
+		this.removeAllListeners();
+		this.stream.send('disconnect', { id: this.id });
+		this.stream.disconnectToChannel(this);
+	}
+}
diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts
deleted file mode 100644
index 50fff05737..0000000000
--- a/src/client/app/common/scripts/streaming/drive.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Drive stream connection
- */
-export class DriveStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'drive', {
-			i: me.token
-		});
-	}
-}
-
-export class DriveStreamManager extends StreamManager<DriveStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new DriveStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
deleted file mode 100644
index adfa75ff3b..0000000000
--- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import Stream from '../../stream';
-import MiOS from '../../../../../mios';
-
-export class ReversiGameStream extends Stream {
-	constructor(os: MiOS, me, game) {
-		super(os, 'games/reversi-game', me ? {
-			i: me.token,
-			game: game.id
-		} : {
-			game: game.id
-		});
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts
deleted file mode 100644
index 1f4fd8c63e..0000000000
--- a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import StreamManager from '../../stream-manager';
-import Stream from '../../stream';
-import MiOS from '../../../../../mios';
-
-export class ReversiStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'games/reversi', {
-			i: me.token
-		});
-	}
-}
-
-export class ReversiStreamManager extends StreamManager<ReversiStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new ReversiStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts
deleted file mode 100644
index a639f1595c..0000000000
--- a/src/client/app/common/scripts/streaming/global-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Global timeline stream connection
- */
-export class GlobalTimelineStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'global-timeline', {
-			i: me.token
-		});
-	}
-}
-
-export class GlobalTimelineStreamManager extends StreamManager<GlobalTimelineStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new GlobalTimelineStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/hashtag.ts b/src/client/app/common/scripts/streaming/hashtag.ts
deleted file mode 100644
index 276b8f8d3d..0000000000
--- a/src/client/app/common/scripts/streaming/hashtag.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../../mios';
-
-export class HashtagStream extends Stream {
-	constructor(os: MiOS, me, q) {
-		super(os, 'hashtag', me ? {
-			i: me.token,
-			q: JSON.stringify(q)
-		} : {
-			q: JSON.stringify(q)
-		});
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
deleted file mode 100644
index 26729507fb..0000000000
--- a/src/client/app/common/scripts/streaming/home.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Home stream connection
- */
-export class HomeStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, '', {
-			i: me.token
-		});
-
-		// 最終利用日時を更新するため定期的にaliveメッセージを送信
-		setInterval(() => {
-			this.send({ type: 'alive' });
-			me.lastUsedAt = new Date();
-		}, 1000 * 60);
-
-		// 自分の情報が更新されたとき
-		this.on('meUpdated', i => {
-			if (os.debug) {
-				console.log('I updated:', i);
-			}
-
-			os.store.dispatch('mergeMe', i);
-		});
-
-		this.on('read_all_notifications', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadNotification: false
-			});
-		});
-
-		this.on('unread_notification', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadNotification: true
-			});
-		});
-
-		this.on('read_all_messaging_messages', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadMessagingMessage: false
-			});
-		});
-
-		this.on('unread_messaging_message', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadMessagingMessage: true
-			});
-		});
-
-		this.on('unreadMention', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadMentions: true
-			});
-		});
-
-		this.on('readAllUnreadMentions', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadMentions: false
-			});
-		});
-
-		this.on('unreadSpecifiedNote', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadSpecifiedNotes: true
-			});
-		});
-
-		this.on('readAllUnreadSpecifiedNotes', () => {
-			os.store.dispatch('mergeMe', {
-				hasUnreadSpecifiedNotes: false
-			});
-		});
-
-		this.on('clientSettingUpdated', x => {
-			os.store.commit('settings/set', {
-				key: x.key,
-				value: x.value
-			});
-		});
-
-		this.on('home_updated', x => {
-			os.store.commit('settings/setHome', x);
-		});
-
-		this.on('mobile_home_updated', x => {
-			os.store.commit('settings/setMobileHome', x);
-		});
-
-		this.on('widgetUpdated', x => {
-			os.store.commit('settings/setWidget', {
-				id: x.id,
-				data: x.data
-			});
-		});
-
-		// トークンが再生成されたとき
-		// このままではMisskeyが利用できないので強制的にサインアウトさせる
-		this.on('my_token_regenerated', () => {
-			alert('%i18n:common.my-token-regenerated%');
-			os.signout();
-		});
-	}
-}
-
-export class HomeStreamManager extends StreamManager<HomeStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new HomeStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts
deleted file mode 100644
index cd290797c4..0000000000
--- a/src/client/app/common/scripts/streaming/hybrid-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Hybrid timeline stream connection
- */
-export class HybridTimelineStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'hybrid-timeline', {
-			i: me.token
-		});
-	}
-}
-
-export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new HybridTimelineStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts
deleted file mode 100644
index 41c36aa14c..0000000000
--- a/src/client/app/common/scripts/streaming/local-timeline.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Local timeline stream connection
- */
-export class LocalTimelineStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'local-timeline', me ? {
-			i: me.token
-		} : {});
-	}
-}
-
-export class LocalTimelineStreamManager extends StreamManager<LocalTimelineStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new LocalTimelineStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts
deleted file mode 100644
index addcccb952..0000000000
--- a/src/client/app/common/scripts/streaming/messaging-index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Messaging index stream connection
- */
-export class MessagingIndexStream extends Stream {
-	constructor(os: MiOS, me) {
-		super(os, 'messaging-index', {
-			i: me.token
-		});
-	}
-}
-
-export class MessagingIndexStreamManager extends StreamManager<MessagingIndexStream> {
-	private me;
-	private os: MiOS;
-
-	constructor(os: MiOS, me) {
-		super();
-
-		this.me = me;
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new MessagingIndexStream(this.os, this.me);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts
deleted file mode 100644
index a59377d867..0000000000
--- a/src/client/app/common/scripts/streaming/messaging.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../../mios';
-
-/**
- * Messaging stream connection
- */
-export class MessagingStream extends Stream {
-	constructor(os: MiOS, me, otherparty) {
-		super(os, 'messaging', {
-			i: me.token,
-			otherparty
-		});
-
-		(this as any).on('_connected_', () => {
-			this.send({
-				i: me.token
-			});
-		});
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/notes-stats.ts b/src/client/app/common/scripts/streaming/notes-stats.ts
deleted file mode 100644
index 9e3e78a709..0000000000
--- a/src/client/app/common/scripts/streaming/notes-stats.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Notes stats stream connection
- */
-export class NotesStatsStream extends Stream {
-	constructor(os: MiOS) {
-		super(os, 'notes-stats');
-	}
-}
-
-export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> {
-	private os: MiOS;
-
-	constructor(os: MiOS) {
-		super();
-
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new NotesStatsStream(this.os);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/server-stats.ts b/src/client/app/common/scripts/streaming/server-stats.ts
deleted file mode 100644
index 9983dfcaf0..0000000000
--- a/src/client/app/common/scripts/streaming/server-stats.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../../mios';
-
-/**
- * Server stats stream connection
- */
-export class ServerStatsStream extends Stream {
-	constructor(os: MiOS) {
-		super(os, 'server-stats');
-	}
-}
-
-export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> {
-	private os: MiOS;
-
-	constructor(os: MiOS) {
-		super();
-
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new ServerStatsStream(this.os);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts
deleted file mode 100644
index 8dd06f67d3..0000000000
--- a/src/client/app/common/scripts/streaming/stream-manager.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { EventEmitter } from 'eventemitter3';
-import * as uuid from 'uuid';
-import Connection from './stream';
-import { erase } from '../../../../../prelude/array';
-
-/**
- * ストリーム接続を管理するクラス
- * 複数の場所から同じストリームを利用する際、接続をまとめたりする
- */
-export default abstract class StreamManager<T extends Connection> extends EventEmitter {
-	private _connection: T = null;
-
-	private disposeTimerId: any;
-
-	/**
-	 * コネクションを必要としているユーザー
-	 */
-	private users = [];
-
-	protected set connection(connection: T) {
-		this._connection = connection;
-
-		if (this._connection == null) {
-			this.emit('disconnected');
-		} else {
-			this.emit('connected', this._connection);
-
-			this._connection.on('_connected_', () => {
-				this.emit('_connected_');
-			});
-
-			this._connection.on('_disconnected_', () => {
-				this.emit('_disconnected_');
-			});
-
-			this._connection.user = 'Managed';
-		}
-	}
-
-	protected get connection() {
-		return this._connection;
-	}
-
-	/**
-	 * コネクションを持っているか否か
-	 */
-	public get hasConnection() {
-		return this._connection != null;
-	}
-
-	public get state(): string {
-		if (!this.hasConnection) return 'no-connection';
-		return this._connection.state;
-	}
-
-	/**
-	 * コネクションを要求します
-	 */
-	public abstract getConnection(): T;
-
-	/**
-	 * 現在接続しているコネクションを取得します
-	 */
-	public borrow() {
-		return this._connection;
-	}
-
-	/**
-	 * コネクションを要求するためのユーザーIDを発行します
-	 */
-	public use() {
-		// タイマー解除
-		if (this.disposeTimerId) {
-			clearTimeout(this.disposeTimerId);
-			this.disposeTimerId = null;
-		}
-
-		// ユーザーID生成
-		const userId = uuid();
-
-		this.users.push(userId);
-
-		this._connection.user = `Managed (${ this.users.length })`;
-
-		return userId;
-	}
-
-	/**
-	 * コネクションを利用し終わってもう必要ないことを通知します
-	 * @param userId use で発行したユーザーID
-	 */
-	public dispose(userId) {
-		this.users = erase(userId, this.users);
-
-		this._connection.user = `Managed (${ this.users.length })`;
-
-		// 誰もコネクションの利用者がいなくなったら
-		if (this.users.length == 0) {
-			// また直ぐに再利用される可能性があるので、一定時間待ち、
-			// 新たな利用者が現れなければコネクションを切断する
-			this.disposeTimerId = setTimeout(() => {
-				this.disposeTimerId = null;
-
-				this.connection.close();
-				this.connection = null;
-			}, 3000);
-		}
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts
deleted file mode 100644
index 4ab78f1190..0000000000
--- a/src/client/app/common/scripts/streaming/stream.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { EventEmitter } from 'eventemitter3';
-import * as uuid from 'uuid';
-import * as ReconnectingWebsocket from 'reconnecting-websocket';
-import { wsUrl } from '../../../config';
-import MiOS from '../../../mios';
-
-/**
- * Misskey stream connection
- */
-export default class Connection extends EventEmitter {
-	public state: string;
-	private buffer: any[];
-	public socket: ReconnectingWebsocket;
-	public name: string;
-	public connectedAt: Date;
-	public user: string = null;
-	public in: number = 0;
-	public out: number = 0;
-	public inout: Array<{
-		type: 'in' | 'out',
-		at: Date,
-		data: string
-	}> = [];
-	public id: string;
-	public isSuspended = false;
-	private os: MiOS;
-
-	constructor(os: MiOS, endpoint, params?) {
-		super();
-
-		//#region BIND
-		this.onOpen =    this.onOpen.bind(this);
-		this.onClose =   this.onClose.bind(this);
-		this.onMessage = this.onMessage.bind(this);
-		this.send =      this.send.bind(this);
-		this.close =     this.close.bind(this);
-		//#endregion
-
-		this.id = uuid();
-		this.os = os;
-		this.name = endpoint;
-		this.state = 'initializing';
-		this.buffer = [];
-
-		const query = params
-			? Object.keys(params)
-				.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
-				.join('&')
-			: null;
-
-		this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? `?${query}` : ''}`);
-		this.socket.addEventListener('open', this.onOpen);
-		this.socket.addEventListener('close', this.onClose);
-		this.socket.addEventListener('message', this.onMessage);
-
-		// Register this connection for debugging
-		this.os.registerStreamConnection(this);
-	}
-
-	/**
-	 * Callback of when open connection
-	 */
-	private onOpen() {
-		this.state = 'connected';
-		this.emit('_connected_');
-
-		this.connectedAt = new Date();
-
-		// バッファーを処理
-		const _buffer = [].concat(this.buffer); // Shallow copy
-		this.buffer = []; // Clear buffer
-		_buffer.forEach(data => {
-			this.send(data); // Resend each buffered messages
-
-			if (this.os.debug) {
-				this.out++;
-				this.inout.push({ type: 'out', at: new Date(), data });
-			}
-		});
-	}
-
-	/**
-	 * Callback of when close connection
-	 */
-	private onClose() {
-		this.state = 'reconnecting';
-		this.emit('_disconnected_');
-	}
-
-	/**
-	 * Callback of when received a message from connection
-	 */
-	private onMessage(message) {
-		if (this.isSuspended) return;
-
-		if (this.os.debug) {
-			this.in++;
-			this.inout.push({ type: 'in', at: new Date(), data: message.data });
-		}
-
-		try {
-			const msg = JSON.parse(message.data);
-			if (msg.type) this.emit(msg.type, msg.body);
-		} catch (e) {
-			// noop
-		}
-	}
-
-	/**
-	 * Send a message to connection
-	 */
-	public send(data) {
-		if (this.isSuspended) return;
-
-		// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
-		if (this.state != 'connected') {
-			this.buffer.push(data);
-			return;
-		}
-
-		if (this.os.debug) {
-			this.out++;
-			this.inout.push({ type: 'out', at: new Date(), data });
-		}
-
-		this.socket.send(JSON.stringify(data));
-	}
-
-	/**
-	 * Close this connection
-	 */
-	public close() {
-		this.os.unregisterStreamConnection(this);
-		this.socket.removeEventListener('open', this.onOpen);
-		this.socket.removeEventListener('message', this.onMessage);
-	}
-}
diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts
deleted file mode 100644
index 30a52b98dd..0000000000
--- a/src/client/app/common/scripts/streaming/user-list.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import Stream from './stream';
-import MiOS from '../../mios';
-
-export class UserListStream extends Stream {
-	constructor(os: MiOS, me, listId) {
-		super(os, 'user-list', {
-			i: me.token,
-			listId
-		});
-
-		(this as any).on('_connected_', () => {
-			this.send({
-				i: me.token
-			});
-		});
-	}
-}
diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
index 1539c88de0..0a18e0b19a 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue
@@ -9,7 +9,6 @@
 import Vue from 'vue';
 import XGame from './reversi.game.vue';
 import XRoom from './reversi.room.vue';
-import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game';
 
 export default Vue.extend({
 	components: {
@@ -34,12 +33,13 @@ export default Vue.extend({
 	},
 	created() {
 		this.g = this.game;
-		this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game);
+		this.connection = (this as any).os.stream.connectToChannel('gamesReversiGame', {
+			gameId: this.game.id
+		});
 		this.connection.on('started', this.onStarted);
 	},
 	beforeDestroy() {
-		this.connection.off('started', this.onStarted);
-		this.connection.close();
+		this.connection.dispose();
 	},
 	methods: {
 		onStarted(game) {
diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index 3725aa6cb4..a040162802 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -59,15 +59,13 @@ export default Vue.extend({
 			myGames: [],
 			matching: null,
 			invitations: [],
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	mounted() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.streams.reversiStream.getConnection();
-			this.connectionId = (this as any).os.streams.reversiStream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
 
 			this.connection.on('invited', this.onInvited);
 
@@ -90,8 +88,7 @@ export default Vue.extend({
 
 	beforeDestroy() {
 		if (this.connection) {
-			this.connection.off('invited', this.onInvited);
-			(this as any).os.streams.reversiStream.dispose(this.connectionId);
+			this.connection.dispose();
 		}
 	},
 
diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue
index 6eb9511ce9..f2156bc41b 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.vue
@@ -47,7 +47,6 @@ export default Vue.extend({
 			game: null,
 			matching: null,
 			connection: null,
-			connectionId: null,
 			pingClock: null
 		};
 	},
@@ -66,8 +65,7 @@ export default Vue.extend({
 		this.fetch();
 
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.streams.reversiStream.getConnection();
-			this.connectionId = (this as any).os.streams.reversiStream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('gamesReversi');
 
 			this.connection.on('matched', this.onMatched);
 
@@ -84,9 +82,7 @@ export default Vue.extend({
 
 	beforeDestroy() {
 		if (this.connection) {
-			this.connection.off('matched', this.onMatched);
-			(this as any).os.streams.reversiStream.dispose(this.connectionId);
-
+			this.connection.dispose();
 			clearInterval(this.pingClock);
 		}
 	},
diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue
index 98661bc39d..c2cd79e116 100644
--- a/src/client/app/common/views/components/messaging-room.vue
+++ b/src/client/app/common/views/components/messaging-room.vue
@@ -30,7 +30,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { MessagingStream } from '../../scripts/streaming/messaging';
 import XMessage from './messaging-room.message.vue';
 import XForm from './messaging-room.form.vue';
 import { url } from '../../../config';
@@ -72,7 +71,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = new MessagingStream((this as any).os, this.$store.state.i, this.user.id);
+		this.connection =((this as any).os.stream.connectToChannel('messaging', { otherparty: this.user.id });
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
@@ -92,9 +91,7 @@ export default Vue.extend({
 	},
 
 	beforeDestroy() {
-		this.connection.off('message', this.onMessage);
-		this.connection.off('read', this.onRead);
-		this.connection.close();
+		this.connection.dispose();
 
 		if (this.isNaked) {
 			window.removeEventListener('scroll', this.onScroll);
@@ -166,6 +163,7 @@ export default Vue.extend({
 		},
 
 		onMessage(message) {
+			console.log(message);
 			// サウンドを再生する
 			if (this.$store.state.device.enableSounds) {
 				const sound = new Audio(`${url}/assets/message.mp3`);
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 91453e16ec..f5b5e232f6 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -71,13 +71,11 @@ export default Vue.extend({
 			messages: [],
 			q: null,
 			result: [],
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	mounted() {
-		this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
-		this.connectionId = (this as any).os.streams.messagingIndexStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('messagingIndex');
 
 		this.connection.on('message', this.onMessage);
 		this.connection.on('read', this.onRead);
@@ -88,9 +86,7 @@ export default Vue.extend({
 		});
 	},
 	beforeDestroy() {
-		this.connection.off('message', this.onMessage);
-		this.connection.off('read', this.onRead);
-		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		getAcct,
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 7025ecea33..9224f82cb9 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -56,7 +56,7 @@ export default Vue.extend({
 				username: this.username,
 				password: this.password,
 				token: this.user && this.user.twoFactorEnabled ? this.token : undefined
-			}).then(() => {
+			}, true).then(() => {
 				location.reload();
 			}).catch(() => {
 				alert('%i18n:@login-failed%');
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index b817ca729d..8e06b13491 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -131,11 +131,11 @@ export default Vue.extend({
 				password: this.password,
 				invitationCode: this.invitationCode,
 				'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
-			}).then(() => {
+			}, true).then(() => {
 				(this as any).api('signin', {
 					username: this.username,
 					password: this.password
-				}).then(() => {
+				}, true).then(() => {
 					location.href = '/';
 				});
 			}).catch(() => {
diff --git a/src/client/app/common/views/components/stream-indicator.vue b/src/client/app/common/views/components/stream-indicator.vue
index d573db32e6..0f25b37cc9 100644
--- a/src/client/app/common/views/components/stream-indicator.vue
+++ b/src/client/app/common/views/components/stream-indicator.vue
@@ -22,7 +22,7 @@ import * as anime from 'animejs';
 export default Vue.extend({
 	computed: {
 		stream() {
-			return (this as any).os.stream;
+			return (this as any).os.stream.useSharedConnection('main');
 		}
 	},
 	created() {
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 6934fb4856..4a66db57b8 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -38,23 +38,20 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			notes: [],
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	mounted() {
 		this.fetch();
 
-		this.connection = (this as any).os.streams.localTimelineStream.getConnection();
-		this.connectionId = (this as any).os.streams.localTimelineStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
 
 		this.connection.on('note', this.onNote);
 	},
 
 	beforeDestroy() {
-		this.connection.off('note', this.onNote);
-		(this as any).os.streams.localTimelineStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/common/views/widgets/photo-stream.vue b/src/client/app/common/views/widgets/photo-stream.vue
index 3e24c58e8e..047b01df4f 100644
--- a/src/client/app/common/views/widgets/photo-stream.vue
+++ b/src/client/app/common/views/widgets/photo-stream.vue
@@ -24,15 +24,13 @@ export default define({
 		return {
 			images: [],
 			fetching: true,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
-		this.connection.on('drive_file_created', this.onDriveFileCreated);
+		this.connection.on('driveFileCreated', this.onDriveFileCreated);
 
 		(this as any).api('drive/stream', {
 			type: 'image/*',
@@ -43,8 +41,7 @@ export default define({
 		});
 	},
 	beforeDestroy() {
-		this.connection.off('drive_file_created', this.onDriveFileCreated);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		onDriveFileCreated(file) {
diff --git a/src/client/app/common/views/widgets/posts-monitor.vue b/src/client/app/common/views/widgets/posts-monitor.vue
index 18df1241a9..1c70e6dbc4 100644
--- a/src/client/app/common/views/widgets/posts-monitor.vue
+++ b/src/client/app/common/views/widgets/posts-monitor.vue
@@ -82,7 +82,6 @@ export default define({
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
 			viewBoxY: 30,
 			stats: [],
 			fediGradientId: uuid(),
@@ -110,8 +109,7 @@ export default define({
 		}
 	},
 	mounted() {
-		this.connection = (this as any).os.streams.notesStatsStream.getConnection();
-		this.connectionId = (this as any).os.streams.notesStatsStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('notesStats');
 
 		this.connection.on('stats', this.onStats);
 		this.connection.on('statsLog', this.onStatsLog);
@@ -121,9 +119,7 @@ export default define({
 		});
 	},
 	beforeDestroy() {
-		this.connection.off('stats', this.onStats);
-		this.connection.off('statsLog', this.onStatsLog);
-		(this as any).os.streams.notesStatsStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		toggle() {
diff --git a/src/client/app/common/views/widgets/server.vue b/src/client/app/common/views/widgets/server.vue
index d796a3ae05..62d75e2bf6 100644
--- a/src/client/app/common/views/widgets/server.vue
+++ b/src/client/app/common/views/widgets/server.vue
@@ -45,8 +45,7 @@ export default define({
 		return {
 			fetching: true,
 			meta: null,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	mounted() {
@@ -55,11 +54,10 @@ export default define({
 			this.fetching = false;
 		});
 
-		this.connection = (this as any).os.streams.serverStatsStream.getConnection();
-		this.connectionId = (this as any).os.streams.serverStatsStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('serverStats');
 	},
 	beforeDestroy() {
-		(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		toggle() {
diff --git a/src/client/app/config.ts b/src/client/app/config.ts
index a326c521db..c3bc427eab 100644
--- a/src/client/app/config.ts
+++ b/src/client/app/config.ts
@@ -12,7 +12,7 @@ export const host = address.host;
 export const hostname = address.hostname;
 export const url = address.origin;
 export const apiUrl = url + '/api';
-export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://');
+export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
 export const lang = _LANG_;
 export const langs = _LANGS_;
 export const themeColor = _THEME_COLOR_;
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 05aa928fa3..85c81d73a2 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -9,7 +9,6 @@ import './style.styl';
 
 import init from '../init';
 import fuckAdBlock from '../common/scripts/fuck-ad-block';
-import { HomeStreamManager } from '../common/scripts/streaming/home';
 import composeNotification from '../common/scripts/compose-notification';
 
 import chooseDriveFolder from './api/choose-drive-folder';
@@ -37,6 +36,7 @@ import MkTag from './views/pages/tag.vue';
 import MkReversi from './views/pages/games/reversi.vue';
 import MkShare from './views/pages/share.vue';
 import MkFollow from '../common/views/pages/follow.vue';
+import MiOS from '../mios';
 
 /**
  * init
@@ -102,62 +102,56 @@ init(async (launch) => {
 		}
 
 		if ((Notification as any).permission == 'granted') {
-			registerNotifications(os.stream);
+			registerNotifications(os);
 		}
 	}
 }, true);
 
-function registerNotifications(stream: HomeStreamManager) {
+function registerNotifications(os: MiOS) {
+	const stream = os.stream;
+
 	if (stream == null) return;
 
-	if (stream.hasConnection) {
-		attach(stream.borrow());
-	}
+	const connection = stream.useSharedConnection('main');
 
-	stream.on('connected', connection => {
-		attach(connection);
+	connection.on('notification', notification => {
+		const _n = composeNotification('notification', notification);
+		const n = new Notification(_n.title, {
+			body: _n.body,
+			icon: _n.icon
+		});
+		setTimeout(n.close.bind(n), 6000);
 	});
 
-	function attach(connection) {
-		connection.on('notification', notification => {
-			const _n = composeNotification('notification', notification);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			setTimeout(n.close.bind(n), 6000);
+	connection.on('driveFileCreated', file => {
+		const _n = composeNotification('driveFileCreated', file);
+		const n = new Notification(_n.title, {
+			body: _n.body,
+			icon: _n.icon
 		});
+		setTimeout(n.close.bind(n), 5000);
+	});
 
-		connection.on('drive_file_created', file => {
-			const _n = composeNotification('drive_file_created', file);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			setTimeout(n.close.bind(n), 5000);
+	connection.on('unreadMessagingMessage', message => {
+		const _n = composeNotification('unreadMessagingMessage', message);
+		const n = new Notification(_n.title, {
+			body: _n.body,
+			icon: _n.icon
 		});
+		n.onclick = () => {
+			n.close();
+			/*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+				user: message.user
+			});*/
+		};
+		setTimeout(n.close.bind(n), 7000);
+	});
 
-		connection.on('unread_messaging_message', message => {
-			const _n = composeNotification('unread_messaging_message', message);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-			n.onclick = () => {
-				n.close();
-				/*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
-					user: message.user
-				});*/
-			};
-			setTimeout(n.close.bind(n), 7000);
+	connection.on('reversiInvited', matching => {
+		const _n = composeNotification('reversiInvited', matching);
+		const n = new Notification(_n.title, {
+			body: _n.body,
+			icon: _n.icon
 		});
-
-		connection.on('reversi_invited', matching => {
-			const _n = composeNotification('reversi_invited', matching);
-			const n = new Notification(_n.title, {
-				body: _n.body,
-				icon: _n.icon
-			});
-		});
-	}
+	});
 }
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index f9b7eea64e..1376a04d99 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -98,8 +98,7 @@ export default Vue.extend({
 			hierarchyFolders: [],
 			selectedFiles: [],
 			uploadings: [],
-			connection: null,
-			connectionId: null,
+			connection: null
 
 			/**
 			 * ドロップされようとしているか
@@ -116,8 +115,7 @@ export default Vue.extend({
 		};
 	},
 	mounted() {
-		this.connection = (this as any).os.streams.driveStream.getConnection();
-		this.connectionId = (this as any).os.streams.driveStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('drive');
 
 		this.connection.on('file_created', this.onStreamDriveFileCreated);
 		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -132,12 +130,7 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		this.connection.off('file_created', this.onStreamDriveFileCreated);
-		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
-		this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
-		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
-		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-		(this as any).os.streams.driveStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		onContextmenu(e) {
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index 4d4bd5cc5a..4d3d61dfe0 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -34,23 +34,18 @@ export default Vue.extend({
 		return {
 			u: this.user,
 			wait: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
-
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 		this.connection.on('follow', this.onFollow);
 		this.connection.on('unfollow', this.onUnfollow);
 	},
 
 	beforeDestroy() {
-		this.connection.off('follow', this.onFollow);
-		this.connection.off('unfollow', this.onUnfollow);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index bdaf2ddf47..9008e26263 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -141,7 +141,6 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
 			widgetAdderSelected: null,
 			trash: []
 		};
@@ -176,12 +175,11 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 	},
 
 	beforeDestroy() {
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 0c4b560e98..b119f23d7a 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -93,12 +93,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './notes.note.sub.vue';
 import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
 	components: {
 		XSub
 	},
 
+	mixins: [noteSubscriber('note')],
+
 	props: {
 		note: {
 			type: Object,
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index b628d045aa..3892260181 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -77,6 +77,7 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './notes.note.sub.vue';
 import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 function focus(el, fn) {
 	const target = fn(el);
@@ -94,6 +95,8 @@ export default Vue.extend({
 		XSub
 	},
 
+	mixins: [noteSubscriber('note')],
+
 	props: {
 		note: {
 			type: Object,
@@ -104,9 +107,7 @@ export default Vue.extend({
 	data() {
 		return {
 			showContent: false,
-			isDetailOpened: false,
-			connection: null,
-			connectionId: null
+			isDetailOpened: false
 		};
 	},
 
@@ -168,86 +169,7 @@ export default Vue.extend({
 		}
 	},
 
-	created() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
 	methods: {
-		capture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				const data = {
-					type: 'capture',
-					id: this.p.id
-				} as any;
-				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
-					data.read = true;
-				}
-				this.connection.send(data);
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-
 		reply(viaKeyboard = false) {
 			(this as any).os.new(MkPostFormWindow, {
 				reply: this.p,
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 1f3f62395a..95b8e1355a 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -118,10 +118,10 @@ export default Vue.extend({
 			notifications: [],
 			moreNotifications: false,
 			connection: null,
-			connectionId: null,
 			getNoteSummary
 		};
 	},
+
 	computed: {
 		_notifications(): any[] {
 			return (this.notifications as any).map(notification => {
@@ -133,9 +133,9 @@ export default Vue.extend({
 			});
 		}
 	},
+
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
 		this.connection.on('notification', this.onNotification);
 
@@ -153,10 +153,11 @@ export default Vue.extend({
 			this.fetching = false;
 		});
 	},
+
 	beforeDestroy() {
-		this.connection.off('notification', this.onNotification);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
+
 	methods: {
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
@@ -177,10 +178,11 @@ export default Vue.extend({
 				this.fetchingMoreNotifications = false;
 			});
 		},
+
 		onNotification(notification) {
 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
 			this.connection.send({
-				type: 'read_notification',
+				type: 'readNotification',
 				id: notification.id
 			});
 
diff --git a/src/client/app/desktop/views/components/settings.signins.vue b/src/client/app/desktop/views/components/settings.signins.vue
index a414c95c27..7d1bb4f4e7 100644
--- a/src/client/app/desktop/views/components/settings.signins.vue
+++ b/src/client/app/desktop/views/components/settings.signins.vue
@@ -23,25 +23,25 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			signins: [],
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
+
 	mounted() {
 		(this as any).api('i/signin_history').then(signins => {
 			this.signins = signins;
 			this.fetching = false;
 		});
 
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
 		this.connection.on('signin', this.onSignin);
 	},
+
 	beforeDestroy() {
-		this.connection.off('signin', this.onSignin);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
+
 	methods: {
 		onSignin(signin) {
 			this.signins.unshift(signin);
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index aef873dd11..2c17e936eb 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -15,7 +15,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
 
 const fetchLimit = 10;
 
@@ -35,9 +34,7 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			streamManager: null,
 			connection: null,
-			connectionId: null,
 			date: null,
 			baseQuery: {
 				includeMyRenotes: this.$store.state.settings.showMyRenotes,
@@ -69,69 +66,33 @@ export default Vue.extend({
 			this.query = {
 				query: this.tagTl.query
 			};
-			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+			this.connection = (this as any).os.stream.connectToChannel('hashtag', { q: this.tagTl.query });
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.connection.close();
-			});
 		} else if (this.src == 'home') {
 			this.endpoint = 'notes/timeline';
 			const onChangeFollowing = () => {
 				this.fetch();
 			};
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('homeTimeline');
 			this.connection.on('note', prepend);
 			this.connection.on('follow', onChangeFollowing);
 			this.connection.on('unfollow', onChangeFollowing);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.connection.off('follow', onChangeFollowing);
-				this.connection.off('unfollow', onChangeFollowing);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'local') {
 			this.endpoint = 'notes/local-timeline';
-			this.streamManager = (this as any).os.streams.localTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'hybrid') {
 			this.endpoint = 'notes/hybrid-timeline';
-			this.streamManager = (this as any).os.streams.hybridTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'global') {
 			this.endpoint = 'notes/global-timeline';
-			this.streamManager = (this as any).os.streams.globalTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'mentions') {
 			this.endpoint = 'notes/mentions';
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 			this.connection.on('mention', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('mention', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'messages') {
 			this.endpoint = 'notes/mentions';
 			this.query = {
@@ -142,21 +103,15 @@ export default Vue.extend({
 					prepend(note);
 				}
 			};
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 			this.connection.on('mention', onNote);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('mention', onNote);
-				this.streamManager.dispose(this.connectionId);
-			});
 		}
 
 		this.fetch();
 	},
 
 	beforeDestroy() {
-		this.$emit('beforeDestroy');
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue
index 4f679db938..122570a696 100644
--- a/src/client/app/desktop/views/components/ui.header.nav.vue
+++ b/src/client/app/desktop/views/components/ui.header.nav.vue
@@ -42,8 +42,7 @@ export default Vue.extend({
 	data() {
 		return {
 			hasGameInvitations: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	computed: {
@@ -53,18 +52,15 @@ export default Vue.extend({
 	},
 	mounted() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 
-			this.connection.on('reversi_invited', this.onReversiInvited);
+			this.connection.on('reversiInvited', this.onReversiInvited);
 			this.connection.on('reversi_no_invites', this.onReversiNoInvites);
 		}
 	},
 	beforeDestroy() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection.off('reversi_invited', this.onReversiInvited);
-			this.connection.off('reversi_no_invites', this.onReversiNoInvites);
-			(this as any).os.stream.dispose(this.connectionId);
+			this.connection.dispose();
 		}
 	},
 	methods: {
diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue
index 0a6f758763..3407851fc5 100644
--- a/src/client/app/desktop/views/components/user-list-timeline.vue
+++ b/src/client/app/desktop/views/components/user-list-timeline.vue
@@ -6,7 +6,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { UserListStream } from '../../../common/scripts/streaming/user-list';
 
 const fetchLimit = 10;
 
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index 1b0c5f8125..c0075220bc 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -56,13 +56,11 @@ export default Vue.extend({
 			disableLocalTimeline: false,
 			bannerUrl: null,
 			inviteCode: null,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	created() {
-		this.connection = (this as any).os.streams.serverStatsStream.getConnection();
-		this.connectionId = (this as any).os.streams.serverStatsStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('serverStats');
 
 		(this as any).os.getMeta().then(meta => {
 			this.disableRegistration = meta.disableRegistration;
@@ -75,7 +73,7 @@ export default Vue.extend({
 		});
 	},
 	beforeDestroy() {
-		(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		invite() {
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue
index ec9e6b9c3d..c771e58a6e 100644
--- a/src/client/app/desktop/views/pages/deck/deck.direct.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue
@@ -21,23 +21,19 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
-
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 		this.connection.on('mention', this.onNote);
 
 		this.fetch();
 	},
 
 	beforeDestroy() {
-		this.connection.off('mention', this.onNote);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
index f38d5a6df5..02d99d3883 100644
--- a/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
@@ -5,7 +5,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import XNotes from './deck.notes.vue';
-import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
 
 const fetchLimit = 10;
 
@@ -48,7 +47,7 @@ export default Vue.extend({
 
 	mounted() {
 		if (this.connection) this.connection.close();
-		this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+		this.connection = (this as any).os.stream.connectToChannel('hashtag', this.tagTl.query);
 		this.connection.on('note', this.onNote);
 
 		this.fetch();
diff --git a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
index e82e76e4d0..e543130310 100644
--- a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
@@ -5,7 +5,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import XNotes from './deck.notes.vue';
-import { UserListStream } from '../../../../common/scripts/streaming/user-list';
 
 const fetchLimit = 10;
 
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions.vue b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
index cecb75f067..17b572f146 100644
--- a/src/client/app/desktop/views/pages/deck/deck.mentions.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
@@ -21,23 +21,19 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
-
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 		this.connection.on('mention', this.onNote);
 
 		this.fetch();
 	},
 
 	beforeDestroy() {
-		this.connection.off('mention', this.onNote);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue
index eac0e78f0f..e843ac54fe 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -70,12 +70,15 @@ import parse from '../../../../../../mfm/parse';
 import MkNoteMenu from '../../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../../common/views/components/reaction-picker.vue';
 import XSub from './deck.note.sub.vue';
+import noteSubscriber from '../../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
 	components: {
 		XSub
 	},
 
+	mixins: [noteSubscriber('note')],
+
 	props: {
 		note: {
 			type: Object,
@@ -90,9 +93,7 @@ export default Vue.extend({
 
 	data() {
 		return {
-			showContent: false,
-			connection: null,
-			connectionId: null
+			showContent: false
 		};
 	},
 
@@ -120,68 +121,7 @@ export default Vue.extend({
 		}
 	},
 
-	created() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
 	methods: {
-		capture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				const data = {
-					type: 'capture',
-					id: this.p.id
-				} as any;
-				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
-					data.read = true;
-				}
-				this.connection.send(data);
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-
 		reply() {
 			(this as any).apis.post({
 				reply: this.p
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index 1417cc3ee8..29de691fe2 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -38,8 +38,7 @@ export default Vue.extend({
 			notifications: [],
 			queue: [],
 			moreNotifications: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
@@ -62,8 +61,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
 		this.connection.on('notification', this.onNotification);
 
@@ -86,8 +84,7 @@ export default Vue.extend({
 	},
 
 	beforeDestroy() {
-		this.connection.off('notification', this.onNotification);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 
 		this.column.$off('top', this.onTop);
 		this.column.$off('bottom', this.onBottom);
@@ -117,7 +114,7 @@ export default Vue.extend({
 		onNotification(notification) {
 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
 			this.connection.send({
-				type: 'read_notification',
+				type: 'readNotification',
 				id: notification.id
 			});
 
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue
index 120ceb7fc2..8aed80fa1b 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue
@@ -36,18 +36,17 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 
 	computed: {
 		stream(): any {
 			switch (this.src) {
-				case 'home': return (this as any).os.stream;
-				case 'local': return (this as any).os.streams.localTimelineStream;
-				case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
-				case 'global': return (this as any).os.streams.globalTimelineStream;
+				case 'home': return (this as any).os.stream.useSharedConnection('homeTimeline');
+				case 'local': return (this as any).os.stream.useSharedConnection('localTimeline');
+				case 'hybrid': return (this as any).os.stream.useSharedConnection('hybridTimeline');
+				case 'global': return (this as any).os.stream.useSharedConnection('globalTimeline');
 			}
 		},
 
@@ -68,8 +67,7 @@ export default Vue.extend({
 	},
 
 	mounted() {
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
+		this.connection = this.stream;
 
 		this.connection.on('note', this.onNote);
 		if (this.src == 'home') {
@@ -81,12 +79,7 @@ export default Vue.extend({
 	},
 
 	beforeDestroy() {
-		this.connection.off('note', this.onNote);
-		if (this.src == 'home') {
-			this.connection.off('follow', this.onChangeFollowing);
-			this.connection.off('unfollow', this.onChangeFollowing);
-		}
-		this.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index ed9e3a6aeb..42171e71fa 100644
--- a/src/client/app/mios.ts
+++ b/src/client/app/mios.ts
@@ -1,3 +1,4 @@
+import autobind from 'autobind-decorator';
 import Vue from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import * as uuid from 'uuid';
@@ -5,19 +6,9 @@ import * as uuid from 'uuid';
 import initStore from './store';
 import { apiUrl, version, lang } from './config';
 import Progress from './common/scripts/loading';
-import Connection from './common/scripts/streaming/stream';
-import { HomeStreamManager } from './common/scripts/streaming/home';
-import { DriveStreamManager } from './common/scripts/streaming/drive';
-import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats';
-import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats';
-import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index';
-import { ReversiStreamManager } from './common/scripts/streaming/games/reversi/reversi';
 
 import Err from './common/views/components/connect-failed.vue';
-import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
-import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
-import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
-import { erase } from '../../prelude/array';
+import Stream from './common/scripts/stream';
 
 //#region api requests
 let spinner = null;
@@ -102,30 +93,7 @@ export default class MiOS extends EventEmitter {
 	/**
 	 * A connection manager of home stream
 	 */
-	public stream: HomeStreamManager;
-
-	/**
-	 * Connection managers
-	 */
-	public streams: {
-		localTimelineStream: LocalTimelineStreamManager;
-		hybridTimelineStream: HybridTimelineStreamManager;
-		globalTimelineStream: GlobalTimelineStreamManager;
-		driveStream: DriveStreamManager;
-		serverStatsStream: ServerStatsStreamManager;
-		notesStatsStream: NotesStatsStreamManager;
-		messagingIndexStream: MessagingIndexStreamManager;
-		reversiStream: ReversiStreamManager;
-	} = {
-		localTimelineStream: null,
-		hybridTimelineStream: null,
-		globalTimelineStream: null,
-		driveStream: null,
-		serverStatsStream: null,
-		notesStatsStream: null,
-		messagingIndexStream: null,
-		reversiStream: null
-	};
+	public stream: Stream;
 
 	/**
 	 * A registration of service worker
@@ -151,71 +119,36 @@ export default class MiOS extends EventEmitter {
 
 		this.shouldRegisterSw = shouldRegisterSw;
 
-		//#region BIND
-		this.log = this.log.bind(this);
-		this.logInfo = this.logInfo.bind(this);
-		this.logWarn = this.logWarn.bind(this);
-		this.logError = this.logError.bind(this);
-		this.init = this.init.bind(this);
-		this.api = this.api.bind(this);
-		this.getMeta = this.getMeta.bind(this);
-		this.registerSw = this.registerSw.bind(this);
-		//#endregion
-
 		if (this.debug) {
 			(window as any).os = this;
 		}
 	}
 
-	private googleMapsIniting = false;
-
-	public getGoogleMaps() {
-		return new Promise((res, rej) => {
-			if ((window as any).google && (window as any).google.maps) {
-				res((window as any).google.maps);
-			} else {
-				this.once('init-google-maps', () => {
-					res((window as any).google.maps);
-				});
-
-				//#region load google maps api
-				if (!this.googleMapsIniting) {
-					this.googleMapsIniting = true;
-					(window as any).initGoogleMaps = () => {
-						this.emit('init-google-maps');
-					};
-					const head = document.getElementsByTagName('head')[0];
-					const script = document.createElement('script');
-					script.setAttribute('src', `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&callback=initGoogleMaps`);
-					script.setAttribute('async', 'true');
-					script.setAttribute('defer', 'true');
-					head.appendChild(script);
-				}
-				//#endregion
-			}
-		});
-	}
-
+	@autobind
 	public log(...args) {
 		if (!this.debug) return;
 		console.log.apply(null, args);
 	}
 
+	@autobind
 	public logInfo(...args) {
 		if (!this.debug) return;
 		console.info.apply(null, args);
 	}
 
+	@autobind
 	public logWarn(...args) {
 		if (!this.debug) return;
 		console.warn.apply(null, args);
 	}
 
+	@autobind
 	public logError(...args) {
 		if (!this.debug) return;
 		console.error.apply(null, args);
 	}
 
+	@autobind
 	public signout() {
 		this.store.dispatch('logout');
 		location.href = '/';
@@ -225,27 +158,10 @@ export default class MiOS extends EventEmitter {
 	 * Initialize MiOS (boot)
 	 * @param callback A function that call when initialized
 	 */
+	@autobind
 	public async init(callback) {
 		this.store = initStore(this);
 
-		//#region Init stream managers
-		this.streams.serverStatsStream = new ServerStatsStreamManager(this);
-		this.streams.notesStatsStream = new NotesStatsStreamManager(this);
-		this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i);
-
-		this.once('signedin', () => {
-			// Init home stream manager
-			this.stream = new HomeStreamManager(this, this.store.state.i);
-
-			// Init other stream manager
-			this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i);
-			this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i);
-			this.streams.driveStream = new DriveStreamManager(this, this.store.state.i);
-			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i);
-			this.streams.reversiStream = new ReversiStreamManager(this, this.store.state.i);
-		});
-		//#endregion
-
 		// ユーザーをフェッチしてコールバックする
 		const fetchme = (token, cb) => {
 			let me = null;
@@ -296,6 +212,8 @@ export default class MiOS extends EventEmitter {
 		const fetched = () => {
 			this.emit('signedin');
 
+			this.stream = new Stream(this);
+
 			// Finish init
 			callback();
 
@@ -328,6 +246,8 @@ export default class MiOS extends EventEmitter {
 				} else {
 					// Finish init
 					callback();
+
+					this.stream = new Stream(this);
 				}
 			});
 		}
@@ -336,6 +256,7 @@ export default class MiOS extends EventEmitter {
 	/**
 	 * Register service worker
 	 */
+	@autobind
 	private registerSw() {
 		// Check whether service worker and push manager supported
 		const isSwSupported =
@@ -418,7 +339,8 @@ export default class MiOS extends EventEmitter {
 	 * @param endpoint エンドポイント名
 	 * @param data パラメータ
 	 */
-	public api(endpoint: string, data: { [x: string]: any } = {}): Promise<{ [x: string]: any }> {
+	@autobind
+	public api(endpoint: string, data: { [x: string]: any } = {}, forceFetch = false): Promise<{ [x: string]: any }> {
 		if (++pending === 1) {
 			spinner = document.createElement('div');
 			spinner.setAttribute('id', 'wait');
@@ -430,13 +352,12 @@ export default class MiOS extends EventEmitter {
 		};
 
 		const promise = new Promise((resolve, reject) => {
-			const viaStream = this.stream && this.stream.hasConnection && this.store.state.device.apiViaStream;
+			const viaStream = this.stream && this.store.state.device.apiViaStream && !forceFetch;
 
 			if (viaStream) {
-				const stream = this.stream.borrow();
 				const id = Math.random().toString();
 
-				stream.once(`api-res:${id}`, res => {
+				this.stream.once(`api:${id}`, res => {
 					if (res == null || Object.keys(res).length == 0) {
 						resolve(null);
 					} else if (res.res) {
@@ -446,11 +367,10 @@ export default class MiOS extends EventEmitter {
 					}
 				});
 
-				stream.send({
-					type: 'api',
-					id,
-					endpoint,
-					data
+				this.stream.send('api', {
+					id: id,
+					ep: endpoint,
+					data: data
 				});
 			} else {
 				// Append a credential
@@ -503,6 +423,7 @@ export default class MiOS extends EventEmitter {
 	 * Misskeyのメタ情報を取得します
 	 * @param force キャッシュを無視するか否か
 	 */
+	@autobind
 	public getMeta(force = false) {
 		return new Promise<{ [x: string]: any }>(async (res, rej) => {
 			if (this.isMetaFetching) {
@@ -530,16 +451,6 @@ export default class MiOS extends EventEmitter {
 			}
 		});
 	}
-
-	public connections: Connection[] = [];
-
-	public registerStreamConnection(connection: Connection) {
-		this.connections.push(connection);
-	}
-
-	public unregisterStreamConnection(connection: Connection) {
-		this.connections = erase(connection, this.connections);
-	}
 }
 
 class WindowSystem extends EventEmitter {
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index 8919462511..469f6da240 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -81,8 +81,7 @@ export default Vue.extend({
 			hierarchyFolders: [],
 			selectedFiles: [],
 			info: null,
-			connection: null,
-			connectionId: null,
+			connection: null
 
 			fetching: true,
 			fetchingMoreFiles: false,
@@ -102,8 +101,7 @@ export default Vue.extend({
 		}
 	},
 	mounted() {
-		this.connection = (this as any).os.streams.driveStream.getConnection();
-		this.connectionId = (this as any).os.streams.driveStream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('drive');
 
 		this.connection.on('file_created', this.onStreamDriveFileCreated);
 		this.connection.on('file_updated', this.onStreamDriveFileUpdated);
@@ -124,12 +122,7 @@ export default Vue.extend({
 		}
 	},
 	beforeDestroy() {
-		this.connection.off('file_created', this.onStreamDriveFileCreated);
-		this.connection.off('file_updated', this.onStreamDriveFileUpdated);
-		this.connection.off('file_deleted', this.onStreamDriveFileDeleted);
-		this.connection.off('folder_created', this.onStreamDriveFolderCreated);
-		this.connection.off('folder_updated', this.onStreamDriveFolderUpdated);
-		(this as any).os.streams.driveStream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 		onStreamDriveFileCreated(file) {
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index aea2d285e4..3c8b2f98e6 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -28,21 +28,17 @@ export default Vue.extend({
 		return {
 			u: this.user,
 			wait: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
 		this.connection.on('follow', this.onFollow);
 		this.connection.on('unfollow', this.onUnfollow);
 	},
 	beforeDestroy() {
-		this.connection.off('follow', this.onFollow);
-		this.connection.off('unfollow', this.onUnfollow);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
 	methods: {
 
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 48d6d32868..082f72f1a9 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -92,12 +92,15 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './note.sub.vue';
 import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
 	components: {
 		XSub
 	},
 
+	mixins: [noteSubscriber('note')],
+
 	props: {
 		note: {
 			type: Object,
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 4af19ade29..db1be4a00e 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -69,19 +69,20 @@ import MkNoteMenu from '../../../common/views/components/note-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './note.sub.vue';
 import { sum } from '../../../../../prelude/array';
+import noteSubscriber from '../../../common/scripts/note-subscriber';
 
 export default Vue.extend({
 	components: {
 		XSub
 	},
 
+	mixins: [noteSubscriber('note')],
+
 	props: ['note'],
 
 	data() {
 		return {
-			showContent: false,
-			connection: null,
-			connectionId: null
+			showContent: false
 		};
 	},
 
@@ -115,86 +116,7 @@ export default Vue.extend({
 		}
 	},
 
-	created() {
-		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
-		}
-	},
-
-	mounted() {
-		this.capture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.on('_connected_', this.onStreamConnected);
-		}
-
-		// Draw map
-		if (this.p.geo) {
-			const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
-			if (shouldShowMap) {
-				(this as any).os.getGoogleMaps().then(maps => {
-					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
-					const map = new maps.Map(this.$refs.map, {
-						center: uluru,
-						zoom: 15
-					});
-					new maps.Marker({
-						position: uluru,
-						map: map
-					});
-				});
-			}
-		}
-	},
-
-	beforeDestroy() {
-		this.decapture(true);
-
-		if (this.$store.getters.isSignedIn) {
-			this.connection.off('_connected_', this.onStreamConnected);
-			(this as any).os.stream.dispose(this.connectionId);
-		}
-	},
-
 	methods: {
-		capture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				const data = {
-					type: 'capture',
-					id: this.p.id
-				} as any;
-				if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
-					data.read = true;
-				}
-				this.connection.send(data);
-				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		decapture(withHandler = false) {
-			if (this.$store.getters.isSignedIn) {
-				this.connection.send({
-					type: 'decapture',
-					id: this.p.id
-				});
-				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
-			}
-		},
-
-		onStreamConnected() {
-			this.capture();
-		},
-
-		onStreamNoteUpdated(data) {
-			const note = data.note;
-			if (note.id == this.note.id) {
-				this.$emit('update:note', note);
-			} else if (note.id == this.note.renoteId) {
-				this.note.renote = note;
-			}
-		},
-
 		reply() {
 			(this as any).apis.post({
 				reply: this.p
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index bfb6c1e62a..e1a2967071 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -23,6 +23,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+
 export default Vue.extend({
 	data() {
 		return {
@@ -30,10 +31,10 @@ export default Vue.extend({
 			fetchingMoreNotifications: false,
 			notifications: [],
 			moreNotifications: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
+
 	computed: {
 		_notifications(): any[] {
 			return (this.notifications as any).map(notification => {
@@ -45,9 +46,9 @@ export default Vue.extend({
 			});
 		}
 	},
+
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
+		this.connection = (this as any).os.stream.useSharedConnection('main');
 
 		this.connection.on('notification', this.onNotification);
 
@@ -66,10 +67,11 @@ export default Vue.extend({
 			this.$emit('fetched');
 		});
 	},
+
 	beforeDestroy() {
-		this.connection.off('notification', this.onNotification);
-		(this as any).os.stream.dispose(this.connectionId);
+		this.connection.dispose();
 	},
+
 	methods: {
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
@@ -90,10 +92,11 @@ export default Vue.extend({
 				this.fetchingMoreNotifications = false;
 			});
 		},
+
 		onNotification(notification) {
 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
 			this.connection.send({
-				type: 'read_notification',
+				type: 'readNotification',
 				id: notification.id
 			});
 
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index 7fd68002e7..9793d03a8c 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -24,44 +24,47 @@ import { env } from '../../../config';
 
 export default Vue.extend({
 	props: ['func'],
+
 	data() {
 		return {
 			hasGameInvitation: false,
 			connection: null,
-			connectionId: null,
 			env: env
 		};
 	},
+
 	computed: {
 		hasUnreadNotification(): boolean {
 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
 		},
+
 		hasUnreadMessagingMessage(): boolean {
 			return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
 		}
 	},
+
 	mounted() {
 		this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight);
 
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 
-			this.connection.on('reversi_invited', this.onReversiInvited);
+			this.connection.on('reversiInvited', this.onReversiInvited);
 			this.connection.on('reversi_no_invites', this.onReversiNoInvites);
 		}
 	},
+
 	beforeDestroy() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection.off('reversi_invited', this.onReversiInvited);
-			this.connection.off('reversi_no_invites', this.onReversiNoInvites);
-			(this as any).os.stream.dispose(this.connectionId);
+			this.connection.dispose();
 		}
 	},
+
 	methods: {
 		onReversiInvited() {
 			this.hasGameInvitation = true;
 		},
+
 		onReversiNoInvites() {
 			this.hasGameInvitation = false;
 		}
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index ba122cc3e2..c9c0c082b2 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -57,7 +57,6 @@ export default Vue.extend({
 		return {
 			hasGameInvitation: false,
 			connection: null,
-			connectionId: null,
 			aboutUrl: `/docs/${lang}/about`,
 			announcements: []
 		};
@@ -79,19 +78,16 @@ export default Vue.extend({
 		});
 
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 
-			this.connection.on('reversi_invited', this.onReversiInvited);
+			this.connection.on('reversiInvited', this.onReversiInvited);
 			this.connection.on('reversi_no_invites', this.onReversiNoInvites);
 		}
 	},
 
 	beforeDestroy() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection.off('reversi_invited', this.onReversiInvited);
-			this.connection.off('reversi_no_invites', this.onReversiNoInvites);
-			(this as any).os.stream.dispose(this.connectionId);
+			this.connection.dispose();
 		}
 	},
 
diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue
index d2af15d235..b16c246b10 100644
--- a/src/client/app/mobile/views/components/ui.vue
+++ b/src/client/app/mobile/views/components/ui.vue
@@ -23,40 +23,43 @@ export default Vue.extend({
 		XHeader,
 		XNav
 	},
+
 	props: ['title'],
+
 	data() {
 		return {
 			isDrawerOpening: false,
-			connection: null,
-			connectionId: null
+			connection: null
 		};
 	},
+
 	watch: {
 		'$store.state.uiHeaderHeight'() {
 			this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
 		}
 	},
+
 	mounted() {
 		this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
 
 		if (this.$store.getters.isSignedIn) {
-			this.connection = (this as any).os.stream.getConnection();
-			this.connectionId = (this as any).os.stream.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 
 			this.connection.on('notification', this.onNotification);
 		}
 	},
+
 	beforeDestroy() {
 		if (this.$store.getters.isSignedIn) {
-			this.connection.off('notification', this.onNotification);
-			(this as any).os.stream.dispose(this.connectionId);
+			this.connection.dispose();
 		}
 	},
+
 	methods: {
 		onNotification(notification) {
 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
 			this.connection.send({
-				type: 'read_notification',
+				type: 'readNotification',
 				id: notification.id
 			});
 
diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue
index 9b3f11f5c2..97200eb5b3 100644
--- a/src/client/app/mobile/views/components/user-list-timeline.vue
+++ b/src/client/app/mobile/views/components/user-list-timeline.vue
@@ -6,7 +6,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { UserListStream } from '../../../common/scripts/streaming/user-list';
 
 const fetchLimit = 10;
 
diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 225abcff6b..1979747bf7 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -13,7 +13,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
 
 const fetchLimit = 10;
 
@@ -35,7 +34,6 @@ export default Vue.extend({
 			existMore: false,
 			streamManager: null,
 			connection: null,
-			connectionId: null,
 			unreadCount: 0,
 			date: null,
 			baseQuery: {
@@ -68,69 +66,33 @@ export default Vue.extend({
 			this.query = {
 				query: this.tagTl.query
 			};
-			this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+			this.connection = (this as any).os.stream.connectToChannel('hashtag', { q: this.tagTl.query });
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.connection.close();
-			});
 		} else if (this.src == 'home') {
 			this.endpoint = 'notes/timeline';
 			const onChangeFollowing = () => {
 				this.fetch();
 			};
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('homeTimeline');
 			this.connection.on('note', prepend);
 			this.connection.on('follow', onChangeFollowing);
 			this.connection.on('unfollow', onChangeFollowing);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.connection.off('follow', onChangeFollowing);
-				this.connection.off('unfollow', onChangeFollowing);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'local') {
 			this.endpoint = 'notes/local-timeline';
-			this.streamManager = (this as any).os.streams.localTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('localTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'hybrid') {
 			this.endpoint = 'notes/hybrid-timeline';
-			this.streamManager = (this as any).os.streams.hybridTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('hybridTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'global') {
 			this.endpoint = 'notes/global-timeline';
-			this.streamManager = (this as any).os.streams.globalTimelineStream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('globalTimeline');
 			this.connection.on('note', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('note', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'mentions') {
 			this.endpoint = 'notes/mentions';
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 			this.connection.on('mention', prepend);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('mention', prepend);
-				this.streamManager.dispose(this.connectionId);
-			});
 		} else if (this.src == 'messages') {
 			this.endpoint = 'notes/mentions';
 			this.query = {
@@ -141,21 +103,15 @@ export default Vue.extend({
 					prepend(note);
 				}
 			};
-			this.streamManager = (this as any).os.stream;
-			this.connection = this.streamManager.getConnection();
-			this.connectionId = this.streamManager.use();
+			this.connection = (this as any).os.stream.useSharedConnection('main');
 			this.connection.on('mention', onNote);
-			this.$once('beforeDestroy', () => {
-				this.connection.off('mention', onNote);
-				this.streamManager.dispose(this.connectionId);
-			});
 		}
 
 		this.fetch();
 	},
 
 	beforeDestroy() {
-		this.$emit('beforeDestroy');
+		this.connection.dispose();
 	},
 
 	methods: {
diff --git a/src/client/app/tsconfig.json b/src/client/app/tsconfig.json
index e31b52dab1..4a05469673 100644
--- a/src/client/app/tsconfig.json
+++ b/src/client/app/tsconfig.json
@@ -14,7 +14,8 @@
     "removeComments": false,
     "noLib": false,
     "strict": true,
-    "strictNullChecks": false
+    "strictNullChecks": false,
+    "experimentalDecorators": true
   },
   "compileOnSave": false,
   "include": [
diff --git a/src/docs/stream.ja-JP.md b/src/docs/stream.ja-JP.md
index c720299932..a8b0eb0cdc 100644
--- a/src/docs/stream.ja-JP.md
+++ b/src/docs/stream.ja-JP.md
@@ -55,7 +55,7 @@ APIへリクエストすると、レスポンスがストリームから次の
 
 ```json
 {
-	type: 'api-res:xxxxxxxxxxxxxxxx',
+	type: 'api:xxxxxxxxxxxxxxxx',
 	body: {
 		...
 	}
@@ -95,7 +95,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
 
 ```json
 {
-	type: 'note-updated',
+	type: 'noteUpdated',
 	body: {
 		note: {
 			...
@@ -108,7 +108,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
 
 ---
 
-このように、投稿の情報が更新されると、`note-updated`イベントが流れてくるようになります。`note-updated`イベントが発生するのは、以下の場合です:
+このように、投稿の情報が更新されると、`noteUpdated`イベントが流れてくるようになります。`noteUpdated`イベントが発生するのは、以下の場合です:
 
 - 投稿にリアクションが付いた
 - 投稿に添付されたアンケートに投票がされた
@@ -153,7 +153,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
 
 `body`プロパティの中に、投稿情報が含まれています。
 
-### `read_all_notifications`
+### `readAllNotifications`
 
 自分宛ての通知がすべて既読になったことを表すイベントです。このイベントを利用して、「通知があることを示すアイコン」のようなものをオフにしたりする等のケースが想定されます。
 
diff --git a/src/notify.ts b/src/notify.ts
index ea7423655e..eaef63e3c3 100644
--- a/src/notify.ts
+++ b/src/notify.ts
@@ -2,7 +2,7 @@ import * as mongo from 'mongodb';
 import Notification from './models/notification';
 import Mute from './models/mute';
 import { pack } from './models/notification';
-import { publishUserStream } from './stream';
+import { publishMainStream } from './stream';
 import User from './models/user';
 import pushSw from './push-sw';
 
@@ -30,7 +30,7 @@ export default (
 	const packed = await pack(notification);
 
 	// Publish notification event
-	publishUserStream(notifiee, 'notification', packed);
+	publishMainStream(notifiee, 'notification', packed);
 
 	// Update flag
 	User.update({ _id: notifiee }, {
@@ -54,7 +54,7 @@ export default (
 			}
 			//#endregion
 
-			publishUserStream(notifiee, 'unread_notification', packed);
+			publishMainStream(notifiee, 'unreadNotification', packed);
 
 			pushSw(notifiee, 'notification', packed);
 		}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index ee79e0a13c..7419bdc95d 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -9,6 +9,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
 
 	const ep = endpoints.find(e => e.name === endpoint);
 
+	if (ep == null) {
+		return rej('ENDPOINT_NOT_FOUND');
+	}
+
 	if (ep.meta.secure && !isSecure) {
 		return rej('ACCESS_DENIED');
 	}
diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts
index 005240a37c..075e369832 100644
--- a/src/server/api/common/read-messaging-message.ts
+++ b/src/server/api/common/read-messaging-message.ts
@@ -1,7 +1,7 @@
 import * as mongo from 'mongodb';
 import Message from '../../../models/messaging-message';
 import { IMessagingMessage as IMessage } from '../../../models/messaging-message';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 import { publishMessagingStream } from '../../../stream';
 import { publishMessagingIndexStream } from '../../../stream';
 import User from '../../../models/user';
@@ -71,6 +71,6 @@ export default (
 		});
 
 		// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
-		publishUserStream(userId, 'read_all_messaging_messages');
+		publishMainStream(userId, 'readAllMessagingMessages');
 	}
 });
diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts
index 0b0f3e4e5a..2d58ada4ce 100644
--- a/src/server/api/common/read-notification.ts
+++ b/src/server/api/common/read-notification.ts
@@ -1,6 +1,6 @@
 import * as mongo from 'mongodb';
 import { default as Notification, INotification } from '../../../models/notification';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 import Mute from '../../../models/mute';
 import User from '../../../models/user';
 
@@ -66,6 +66,6 @@ export default (
 		});
 
 		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
-		publishUserStream(userId, 'read_all_notifications');
+		publishMainStream(userId, 'readAllNotifications');
 	}
 });
diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts
index aba400af1d..d7483a0bfd 100644
--- a/src/server/api/endpoints/games/reversi/match.ts
+++ b/src/server/api/endpoints/games/reversi/match.ts
@@ -2,7 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
 import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching';
 import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game';
 import User, { ILocalUser } from '../../../../../models/user';
-import { publishUserStream, publishReversiStream } from '../../../../../stream';
+import { publishMainStream, publishReversiStream } from '../../../../../stream';
 import { eighteight } from '../../../../../games/reversi/maps';
 
 export const meta = {
@@ -58,7 +58,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		});
 
 		if (other == 0) {
-			publishUserStream(user._id, 'reversi_no_invites');
+			publishMainStream(user._id, 'reversi_no_invites');
 		}
 	} else {
 		// Fetch child
@@ -94,6 +94,6 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		// 招待
 		publishReversiStream(child._id, 'invited', packed);
 
-		publishUserStream(child._id, 'reversi_invited', packed);
+		publishMainStream(child._id, 'reversiInvited', packed);
 	}
 });
diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts
index fe4a5cd118..2d85f06cfa 100644
--- a/src/server/api/endpoints/i/regenerate_token.ts
+++ b/src/server/api/endpoints/i/regenerate_token.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import * as bcrypt from 'bcryptjs';
 import User, { ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 import generateUserToken from '../../common/generate-native-user-token';
 
 export const meta = {
@@ -33,5 +33,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 	res();
 
 	// Publish event
-	publishUserStream(user._id, 'my_token_regenerated');
+	publishMainStream(user._id, 'myTokenRegenerated');
 });
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index c1be0b6ebc..548ce5cadb 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
 import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack, ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 import DriveFile from '../../../../models/drive-file';
 import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
 import { IApp } from '../../../../models/app';
@@ -177,7 +177,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
 	res(iObj);
 
 	// Publish meUpdated event
-	publishUserStream(user._id, 'meUpdated', iObj);
+	publishMainStream(user._id, 'meUpdated', iObj);
 
 	// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
 	if (user.isLocked && ps.isLocked === false) {
diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts
index aed93c792f..2c05299dff 100644
--- a/src/server/api/endpoints/i/update_client_setting.ts
+++ b/src/server/api/endpoints/i/update_client_setting.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import User, { ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 
 export const meta = {
 	requireCredential: true,
@@ -26,7 +26,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 	res();
 
 	// Publish event
-	publishUserStream(user._id, 'clientSettingUpdated', {
+	publishMainStream(user._id, 'clientSettingUpdated', {
 		key: name,
 		value
 	});
diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts
index ffca9b90b3..27afc9fe5a 100644
--- a/src/server/api/endpoints/i/update_home.ts
+++ b/src/server/api/endpoints/i/update_home.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import User, { ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 
 export const meta = {
 	requireCredential: true,
@@ -25,5 +25,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 
 	res();
 
-	publishUserStream(user._id, 'home_updated', home);
+	publishMainStream(user._id, 'homeUpdated', home);
 });
diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts
index 0b72fbe2c1..1d4df389e4 100644
--- a/src/server/api/endpoints/i/update_mobile_home.ts
+++ b/src/server/api/endpoints/i/update_mobile_home.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import User, { ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 
 export const meta = {
 	requireCredential: true,
@@ -24,5 +24,5 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 
 	res();
 
-	publishUserStream(user._id, 'mobile_home_updated', home);
+	publishMainStream(user._id, 'mobileHomeUpdated', home);
 });
diff --git a/src/server/api/endpoints/i/update_widget.ts b/src/server/api/endpoints/i/update_widget.ts
index 5cbe7c07a3..92499493eb 100644
--- a/src/server/api/endpoints/i/update_widget.ts
+++ b/src/server/api/endpoints/i/update_widget.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import User, { ILocalUser } from '../../../../models/user';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 
 export const meta = {
 	requireCredential: true,
@@ -73,7 +73,7 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 	//#endregion
 
 	if (widget) {
-		publishUserStream(user._id, 'widgetUpdated', {
+		publishMainStream(user._id, 'widgetUpdated', {
 			id, data
 		});
 
diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts
index 9a49e09248..f504f92326 100644
--- a/src/server/api/endpoints/messaging/messages/create.ts
+++ b/src/server/api/endpoints/messaging/messages/create.ts
@@ -6,7 +6,7 @@ import User, { ILocalUser } from '../../../../../models/user';
 import Mute from '../../../../../models/mute';
 import DriveFile from '../../../../../models/drive-file';
 import { pack } from '../../../../../models/messaging-message';
-import { publishUserStream } from '../../../../../stream';
+import { publishMainStream } from '../../../../../stream';
 import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../stream';
 import pushSw from '../../../../../push-sw';
 
@@ -88,12 +88,12 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	// 自分のストリーム
 	publishMessagingStream(message.userId, message.recipientId, 'message', messageObj);
 	publishMessagingIndexStream(message.userId, 'message', messageObj);
-	publishUserStream(message.userId, 'messaging_message', messageObj);
+	publishMainStream(message.userId, 'messagingMessage', messageObj);
 
 	// 相手のストリーム
 	publishMessagingStream(message.recipientId, message.userId, 'message', messageObj);
 	publishMessagingIndexStream(message.recipientId, 'message', messageObj);
-	publishUserStream(message.recipientId, 'messaging_message', messageObj);
+	publishMainStream(message.recipientId, 'messagingMessage', messageObj);
 
 	// Update flag
 	User.update({ _id: recipient._id }, {
@@ -117,8 +117,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 			}
 			//#endregion
 
-			publishUserStream(message.recipientId, 'unread_messaging_message', messageObj);
-			pushSw(message.recipientId, 'unread_messaging_message', messageObj);
+			publishMainStream(message.recipientId, 'unreadMessagingMessage', messageObj);
+			pushSw(message.recipientId, 'unreadMessagingMessage', messageObj);
 		}
 	}, 3000);
 
diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts
index ab80e7f5d0..3b78d62fd3 100644
--- a/src/server/api/endpoints/notes/polls/vote.ts
+++ b/src/server/api/endpoints/notes/polls/vote.ts
@@ -72,7 +72,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 		$inc: inc
 	});
 
-	publishNoteStream(note._id, 'poll_voted');
+	publishNoteStream(note._id, 'pollVoted', {
+		choice: choice,
+		userId: user._id.toHexString()
+	});
 
 	// Notify
 	notify(note.userId, user._id, 'poll_vote', {
diff --git a/src/server/api/endpoints/notifications/mark_all_as_read.ts b/src/server/api/endpoints/notifications/mark_all_as_read.ts
index e2bde777b3..6487cd8b48 100644
--- a/src/server/api/endpoints/notifications/mark_all_as_read.ts
+++ b/src/server/api/endpoints/notifications/mark_all_as_read.ts
@@ -1,5 +1,5 @@
 import Notification from '../../../../models/notification';
-import { publishUserStream } from '../../../../stream';
+import { publishMainStream } from '../../../../stream';
 import User, { ILocalUser } from '../../../../models/user';
 
 export const meta = {
@@ -40,5 +40,5 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
 	});
 
 	// 全ての通知を読みましたよというイベントを発行
-	publishUserStream(user._id, 'read_all_notifications');
+	publishMainStream(user._id, 'readAllNotifications');
 });
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index c42fb7bd8c..0e44c2ddd6 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
 import * as speakeasy from 'speakeasy';
 import User, { ILocalUser } from '../../../models/user';
 import Signin, { pack } from '../../../models/signin';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 import signin from '../common/signin';
 import config from '../../../config';
 
@@ -87,5 +87,5 @@ export default async (ctx: Koa.Context) => {
 	});
 
 	// Publish signin event
-	publishUserStream(user._id, 'signin', await pack(record));
+	publishMainStream(user._id, 'signin', await pack(record));
 };
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index aad2846bb4..f71e588628 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -4,7 +4,7 @@ import * as uuid from 'uuid';
 import autwh from 'autwh';
 import redis from '../../../db/redis';
 import User, { pack, ILocalUser } from '../../../models/user';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 import config from '../../../config';
 import signin from '../common/signin';
 
@@ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => {
 	ctx.body = `Twitterの連携を解除しました :v:`;
 
 	// Publish i updated event
-	publishUserStream(user._id, 'meUpdated', await pack(user, user, {
+	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 		detail: true,
 		includeSecrets: true
 	}));
@@ -174,7 +174,7 @@ if (config.twitter == null) {
 			ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
 
 			// Publish i updated event
-			publishUserStream(user._id, 'meUpdated', await pack(user, user, {
+			publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 				detail: true,
 				includeSecrets: true
 			}));
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
new file mode 100644
index 0000000000..e2726060dc
--- /dev/null
+++ b/src/server/api/stream/channel.ts
@@ -0,0 +1,39 @@
+import autobind from 'autobind-decorator';
+import Connection from '.';
+
+/**
+ * Stream channel
+ */
+export default abstract class Channel {
+	protected connection: Connection;
+	public id: string;
+
+	protected get user() {
+		return this.connection.user;
+	}
+
+	protected get subscriber() {
+		return this.connection.subscriber;
+	}
+
+	constructor(id: string, connection: Connection) {
+		this.id = id;
+		this.connection = connection;
+	}
+
+	@autobind
+	public send(typeOrPayload: any, payload?: any) {
+		const type = payload === undefined ? typeOrPayload.type : typeOrPayload;
+		const body = payload === undefined ? typeOrPayload.body : payload;
+
+		this.connection.sendMessageToWs('channel', {
+			id: this.id,
+			type: type,
+			body: body
+		});
+	}
+
+	public abstract init(params: any): void;
+	public dispose?(): void;
+	public onMessage?(type: string, body: any): void;
+}
diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts
new file mode 100644
index 0000000000..807fc93cd0
--- /dev/null
+++ b/src/server/api/stream/channels/drive.ts
@@ -0,0 +1,12 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		// Subscribe drive stream
+		this.subscriber.on(`driveStream:${this.user._id}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
new file mode 100644
index 0000000000..11f1fb1feb
--- /dev/null
+++ b/src/server/api/stream/channels/games/reversi-game.ts
@@ -0,0 +1,309 @@
+import autobind from 'autobind-decorator';
+import * as CRC32 from 'crc-32';
+import ReversiGame, { pack } from '../../../../../models/games/reversi/game';
+import { publishReversiGameStream } from '../../../../../stream';
+import Reversi from '../../../../../games/reversi/core';
+import * as maps from '../../../../../games/reversi/maps';
+import Channel from '../../channel';
+
+export default class extends Channel {
+	private gameId: string;
+
+	@autobind
+	public async init(params: any) {
+		this.gameId = params.gameId as string;
+
+		// Subscribe game stream
+		this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
+			this.send(data);
+		});
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'accept': this.accept(true); break;
+			case 'cancel-accept': this.accept(false); break;
+			case 'update-settings': this.updateSettings(body.settings); break;
+			case 'init-form': this.initForm(body); break;
+			case 'update-form': this.updateForm(body.id, body.value); break;
+			case 'message': this.message(body); break;
+			case 'set': this.set(body.pos); break;
+			case 'check': this.check(body.crc32); break;
+		}
+	}
+
+	@autobind
+	private async updateSettings(settings: any) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+		if (game.user1Id.equals(this.user._id) && game.user1Accepted) return;
+		if (game.user2Id.equals(this.user._id) && game.user2Accepted) return;
+
+		await ReversiGame.update({ _id: this.gameId }, {
+			$set: {
+				settings
+			}
+		});
+
+		publishReversiGameStream(this.gameId, 'updateSettings', settings);
+	}
+
+	@autobind
+	private async initForm(form: any) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+
+		const set = game.user1Id.equals(this.user._id) ? {
+			form1: form
+		} : {
+				form2: form
+			};
+
+		await ReversiGame.update({ _id: this.gameId }, {
+			$set: set
+		});
+
+		publishReversiGameStream(this.gameId, 'initForm', {
+			userId: this.user._id,
+			form
+		});
+	}
+
+	@autobind
+	private async updateForm(id: string, value: any) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (game.isStarted) return;
+		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+
+		const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1;
+
+		const item = form.find((i: any) => i.id == id);
+
+		if (item == null) return;
+
+		item.value = value;
+
+		const set = game.user1Id.equals(this.user._id) ? {
+			form2: form
+		} : {
+				form1: form
+			};
+
+		await ReversiGame.update({ _id: this.gameId }, {
+			$set: set
+		});
+
+		publishReversiGameStream(this.gameId, 'updateForm', {
+			userId: this.user._id,
+			id,
+			value
+		});
+	}
+
+	@autobind
+	private async message(message: any) {
+		message.id = Math.random();
+		publishReversiGameStream(this.gameId, 'message', {
+			userId: this.user._id,
+			message
+		});
+	}
+
+	@autobind
+	private async accept(accept: boolean) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (game.isStarted) return;
+
+		let bothAccepted = false;
+
+		if (game.user1Id.equals(this.user._id)) {
+			await ReversiGame.update({ _id: this.gameId }, {
+				$set: {
+					user1Accepted: accept
+				}
+			});
+
+			publishReversiGameStream(this.gameId, 'changeAccepts', {
+				user1: accept,
+				user2: game.user2Accepted
+			});
+
+			if (accept && game.user2Accepted) bothAccepted = true;
+		} else if (game.user2Id.equals(this.user._id)) {
+			await ReversiGame.update({ _id: this.gameId }, {
+				$set: {
+					user2Accepted: accept
+				}
+			});
+
+			publishReversiGameStream(this.gameId, 'changeAccepts', {
+				user1: game.user1Accepted,
+				user2: accept
+			});
+
+			if (accept && game.user1Accepted) bothAccepted = true;
+		} else {
+			return;
+		}
+
+		if (bothAccepted) {
+			// 3秒後、まだacceptされていたらゲーム開始
+			setTimeout(async () => {
+				const freshGame = await ReversiGame.findOne({ _id: this.gameId });
+				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
+				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
+
+				let bw: number;
+				if (freshGame.settings.bw == 'random') {
+					bw = Math.random() > 0.5 ? 1 : 2;
+				} else {
+					bw = freshGame.settings.bw as number;
+				}
+
+				function getRandomMap() {
+					const mapCount = Object.entries(maps).length;
+					const rnd = Math.floor(Math.random() * mapCount);
+					return Object.values(maps)[rnd].data;
+				}
+
+				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
+
+				await ReversiGame.update({ _id: this.gameId }, {
+					$set: {
+						startedAt: new Date(),
+						isStarted: true,
+						black: bw,
+						'settings.map': map
+					}
+				});
+
+				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
+				const o = new Reversi(map, {
+					isLlotheo: freshGame.settings.isLlotheo,
+					canPutEverywhere: freshGame.settings.canPutEverywhere,
+					loopedBoard: freshGame.settings.loopedBoard
+				});
+
+				if (o.isEnded) {
+					let winner;
+					if (o.winner === true) {
+						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
+					} else if (o.winner === false) {
+						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
+					} else {
+						winner = null;
+					}
+
+					await ReversiGame.update({
+						_id: this.gameId
+					}, {
+							$set: {
+								isEnded: true,
+								winnerId: winner
+							}
+						});
+
+					publishReversiGameStream(this.gameId, 'ended', {
+						winnerId: winner,
+						game: await pack(this.gameId, this.user)
+					});
+				}
+				//#endregion
+
+				publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user));
+			}, 3000);
+		}
+	}
+
+	// 石を打つ
+	@autobind
+	private async set(pos: number) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (!game.isStarted) return;
+		if (game.isEnded) return;
+		if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return;
+
+		const o = new Reversi(game.settings.map, {
+			isLlotheo: game.settings.isLlotheo,
+			canPutEverywhere: game.settings.canPutEverywhere,
+			loopedBoard: game.settings.loopedBoard
+		});
+
+		game.logs.forEach(log => {
+			o.put(log.color, log.pos);
+		});
+
+		const myColor =
+			(game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2)
+				? true
+				: false;
+
+		if (!o.canPut(myColor, pos)) return;
+		o.put(myColor, pos);
+
+		let winner;
+		if (o.isEnded) {
+			if (o.winner === true) {
+				winner = game.black == 1 ? game.user1Id : game.user2Id;
+			} else if (o.winner === false) {
+				winner = game.black == 1 ? game.user2Id : game.user1Id;
+			} else {
+				winner = null;
+			}
+		}
+
+		const log = {
+			at: new Date(),
+			color: myColor,
+			pos
+		};
+
+		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
+
+		await ReversiGame.update({
+			_id: this.gameId
+		}, {
+				$set: {
+					crc32,
+					isEnded: o.isEnded,
+					winnerId: winner
+				},
+				$push: {
+					logs: log
+				}
+			});
+
+		publishReversiGameStream(this.gameId, 'set', Object.assign(log, {
+			next: o.turn
+		}));
+
+		if (o.isEnded) {
+			publishReversiGameStream(this.gameId, 'ended', {
+				winnerId: winner,
+				game: await pack(this.gameId, this.user)
+			});
+		}
+	}
+
+	@autobind
+	private async check(crc32: string) {
+		const game = await ReversiGame.findOne({ _id: this.gameId });
+
+		if (!game.isStarted) return;
+
+		// 互換性のため
+		if (game.crc32 == null) return;
+
+		if (crc32 !== game.crc32) {
+			this.send('rescue', await pack(game, this.user));
+		}
+	}
+}
diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts
new file mode 100644
index 0000000000..d75025c944
--- /dev/null
+++ b/src/server/api/stream/channels/games/reversi.ts
@@ -0,0 +1,30 @@
+import autobind from 'autobind-decorator';
+import * as mongo from 'mongodb';
+import Matching, { pack } from '../../../../../models/games/reversi/matching';
+import { publishMainStream } from '../../../../../stream';
+import Channel from '../../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		// Subscribe reversi stream
+		this.subscriber.on(`reversiStream:${this.user._id}`, data => {
+			this.send(data);
+		});
+	}
+
+	@autobind
+	public async onMessage(type: string, body: any) {
+		switch (type) {
+			case 'ping':
+				if (body.id == null) return;
+				const matching = await Matching.findOne({
+					parentId: this.user._id,
+					childId: new mongo.ObjectID(body.id)
+				});
+				if (matching == null) return;
+				publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId));
+				break;
+		}
+	}
+}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
new file mode 100644
index 0000000000..ab0fe5d094
--- /dev/null
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -0,0 +1,39 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+
+export default class extends Channel {
+	private mutedUserIds: string[] = [];
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on('globalTimeline', this.onNote);
+
+		const mute = await Mute.find({ muterId: this.user._id });
+		this.mutedUserIds = mute.map(m => m.muteeId.toString());
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('globalTimeline', this.onNote);
+	}
+}
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
new file mode 100644
index 0000000000..652b0caa5b
--- /dev/null
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -0,0 +1,33 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
+		const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+
+		const q: Array<string[]> = params.q;
+
+		// Subscribe stream
+		this.subscriber.on('hashtag', async note => {
+			const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
+			if (!matched) return;
+
+			// Renoteなら再pack
+			if (note.renoteId != null) {
+				note.renote = await pack(note.renoteId, this.user, {
+					detail: true
+				});
+			}
+
+			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+			if (shouldMuteThisNote(note, mutedUserIds)) return;
+
+			this.send('note', note);
+		});
+	}
+}
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
new file mode 100644
index 0000000000..4c674e75ef
--- /dev/null
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -0,0 +1,39 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+
+export default class extends Channel {
+	private mutedUserIds: string[] = [];
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote);
+
+		const mute = await Mute.find({ muterId: this.user._id });
+		this.mutedUserIds = mute.map(m => m.muteeId.toString());
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote);
+	}
+}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
new file mode 100644
index 0000000000..0b12ab3a8f
--- /dev/null
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -0,0 +1,41 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+
+export default class extends Channel {
+	private mutedUserIds: string[] = [];
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on('hybridTimeline', this.onNewNote);
+		this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote);
+
+		const mute = await Mute.find({ muterId: this.user._id });
+		this.mutedUserIds = mute.map(m => m.muteeId.toString());
+	}
+
+	@autobind
+	private async onNewNote(note: any) {
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('hybridTimeline', this.onNewNote);
+		this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote);
+	}
+}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
new file mode 100644
index 0000000000..7e71590d00
--- /dev/null
+++ b/src/server/api/stream/channels/index.ts
@@ -0,0 +1,31 @@
+import main from './main';
+import homeTimeline from './home-timeline';
+import localTimeline from './local-timeline';
+import hybridTimeline from './hybrid-timeline';
+import globalTimeline from './global-timeline';
+import notesStats from './notes-stats';
+import serverStats from './server-stats';
+import userList from './user-list';
+import messaging from './messaging';
+import messagingIndex from './messaging-index';
+import drive from './drive';
+import hashtag from './hashtag';
+import gamesReversi from './games/reversi';
+import gamesReversiGame from './games/reversi-game';
+
+export default {
+	main,
+	homeTimeline,
+	localTimeline,
+	hybridTimeline,
+	globalTimeline,
+	notesStats,
+	serverStats,
+	userList,
+	messaging,
+	messagingIndex,
+	drive,
+	hashtag,
+	gamesReversi,
+	gamesReversiGame
+};
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
new file mode 100644
index 0000000000..769ec6392f
--- /dev/null
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -0,0 +1,39 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import Channel from '../channel';
+
+export default class extends Channel {
+	private mutedUserIds: string[] = [];
+
+	@autobind
+	public async init(params: any) {
+		// Subscribe events
+		this.subscriber.on('localTimeline', this.onNote);
+
+		const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null;
+		this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+	}
+
+	@autobind
+	private async onNote(note: any) {
+		// Renoteなら再pack
+		if (note.renoteId != null) {
+			note.renote = await pack(note.renoteId, this.user, {
+				detail: true
+			});
+		}
+
+		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (shouldMuteThisNote(note, this.mutedUserIds)) return;
+
+		this.send('note', note);
+	}
+
+	@autobind
+	public dispose() {
+		// Unsubscribe events
+		this.subscriber.off('localTimeline', this.onNote);
+	}
+}
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
new file mode 100644
index 0000000000..a6c5b12760
--- /dev/null
+++ b/src/server/api/stream/channels/main.ts
@@ -0,0 +1,25 @@
+import autobind from 'autobind-decorator';
+import Mute from '../../../../models/mute';
+import Channel from '../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		const mute = await Mute.find({ muterId: this.user._id });
+		const mutedUserIds = mute.map(m => m.muteeId.toString());
+
+		// Subscribe main stream channel
+		this.subscriber.on(`mainStream:${this.user._id}`, async data => {
+			const { type, body } = data;
+
+			switch (type) {
+				case 'notification': {
+					if (!mutedUserIds.includes(body.userId)) {
+						this.send('notification', body);
+					}
+					break;
+				}
+			}
+		});
+	}
+}
diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts
new file mode 100644
index 0000000000..6e87cca7f4
--- /dev/null
+++ b/src/server/api/stream/channels/messaging-index.ts
@@ -0,0 +1,12 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		// Subscribe messaging index stream
+		this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts
new file mode 100644
index 0000000000..e1a78c8678
--- /dev/null
+++ b/src/server/api/stream/channels/messaging.ts
@@ -0,0 +1,26 @@
+import autobind from 'autobind-decorator';
+import read from '../../common/read-messaging-message';
+import Channel from '../channel';
+
+export default class extends Channel {
+	private otherpartyId: string;
+
+	@autobind
+	public async init(params: any) {
+		this.otherpartyId = params.otherparty as string;
+
+		// Subscribe messaging stream
+		this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => {
+			this.send(data);
+		});
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'read':
+				read(this.user._id, this.otherpartyId, body.id);
+				break;
+		}
+	}
+}
diff --git a/src/server/api/stream/channels/notes-stats.ts b/src/server/api/stream/channels/notes-stats.ts
new file mode 100644
index 0000000000..cc68d9886d
--- /dev/null
+++ b/src/server/api/stream/channels/notes-stats.ts
@@ -0,0 +1,34 @@
+import autobind from 'autobind-decorator';
+import Xev from 'xev';
+import Channel from '../channel';
+
+const ev = new Xev();
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		ev.addListener('notesStats', this.onStats);
+	}
+
+	@autobind
+	private onStats(stats: any) {
+		this.send('stats', stats);
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'requestLog':
+				ev.once(`notesStatsLog:${body.id}`, statsLog => {
+					this.send('statsLog', statsLog);
+				});
+				ev.emit('requestNotesStatsLog', body.id);
+				break;
+		}
+	}
+
+	@autobind
+	public dispose() {
+		ev.removeListener('notesStats', this.onStats);
+	}
+}
diff --git a/src/server/api/stream/channels/server-stats.ts b/src/server/api/stream/channels/server-stats.ts
new file mode 100644
index 0000000000..28a566e8ae
--- /dev/null
+++ b/src/server/api/stream/channels/server-stats.ts
@@ -0,0 +1,37 @@
+import autobind from 'autobind-decorator';
+import Xev from 'xev';
+import Channel from '../channel';
+
+const ev = new Xev();
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		ev.addListener('serverStats', this.onStats);
+	}
+
+	@autobind
+	private onStats(stats: any) {
+		this.send('stats', stats);
+	}
+
+	@autobind
+	public onMessage(type: string, body: any) {
+		switch (type) {
+			case 'requestLog':
+				ev.once(`serverStatsLog:${body.id}`, statsLog => {
+					this.send('statsLog', statsLog);
+				});
+				ev.emit('requestServerStatsLog', {
+					id: body.id,
+					length: body.length
+				});
+				break;
+		}
+	}
+
+	@autobind
+	public dispose() {
+		ev.removeListener('serverStats', this.onStats);
+	}
+}
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
new file mode 100644
index 0000000000..4ace308923
--- /dev/null
+++ b/src/server/api/stream/channels/user-list.ts
@@ -0,0 +1,14 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+	@autobind
+	public async init(params: any) {
+		const listId = params.listId as string;
+
+		// Subscribe stream
+		this.subscriber.on(`userListStream:${listId}`, data => {
+			this.send(data);
+		});
+	}
+}
diff --git a/src/server/api/stream/drive.ts b/src/server/api/stream/drive.ts
deleted file mode 100644
index 28c241e1bc..0000000000
--- a/src/server/api/stream/drive.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void {
-	// Subscribe drive stream
-	subscriber.on(`drive-stream:${user._id}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-}
diff --git a/src/server/api/stream/games/reversi-game.ts b/src/server/api/stream/games/reversi-game.ts
deleted file mode 100644
index 5cbbf42d59..0000000000
--- a/src/server/api/stream/games/reversi-game.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-import * as CRC32 from 'crc-32';
-import ReversiGame, { pack } from '../../../../models/games/reversi/game';
-import { publishReversiGameStream } from '../../../../stream';
-import Reversi from '../../../../games/reversi/core';
-import * as maps from '../../../../games/reversi/maps';
-import { ParsedUrlQuery } from 'querystring';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user?: any): void {
-	const q = request.resourceURL.query as ParsedUrlQuery;
-	const gameId = q.game as string;
-
-	// Subscribe game stream
-	subscriber.on(`reversi-game-stream:${gameId}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-
-	connection.on('message', async (data) => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'accept':
-				accept(true);
-				break;
-
-			case 'cancel-accept':
-				accept(false);
-				break;
-
-			case 'update-settings':
-				if (msg.settings == null) return;
-				updateSettings(msg.settings);
-				break;
-
-			case 'init-form':
-				if (msg.body == null) return;
-				initForm(msg.body);
-				break;
-
-			case 'update-form':
-				if (msg.id == null || msg.value === undefined) return;
-				updateForm(msg.id, msg.value);
-				break;
-
-			case 'message':
-				if (msg.body == null) return;
-				message(msg.body);
-				break;
-
-			case 'set':
-				if (msg.pos == null) return;
-				set(msg.pos);
-				break;
-
-			case 'check':
-				if (msg.crc32 == null) return;
-				check(msg.crc32);
-				break;
-		}
-	});
-
-	async function updateSettings(settings: any) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (game.isStarted) return;
-		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
-		if (game.user1Id.equals(user._id) && game.user1Accepted) return;
-		if (game.user2Id.equals(user._id) && game.user2Accepted) return;
-
-		await ReversiGame.update({ _id: gameId }, {
-			$set: {
-				settings
-			}
-		});
-
-		publishReversiGameStream(gameId, 'update-settings', settings);
-	}
-
-	async function initForm(form: any) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (game.isStarted) return;
-		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
-
-		const set = game.user1Id.equals(user._id) ? {
-			form1: form
-		} : {
-				form2: form
-			};
-
-		await ReversiGame.update({ _id: gameId }, {
-			$set: set
-		});
-
-		publishReversiGameStream(gameId, 'init-form', {
-			userId: user._id,
-			form
-		});
-	}
-
-	async function updateForm(id: string, value: any) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (game.isStarted) return;
-		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
-
-		const form = game.user1Id.equals(user._id) ? game.form2 : game.form1;
-
-		const item = form.find((i: any) => i.id == id);
-
-		if (item == null) return;
-
-		item.value = value;
-
-		const set = game.user1Id.equals(user._id) ? {
-			form2: form
-		} : {
-				form1: form
-			};
-
-		await ReversiGame.update({ _id: gameId }, {
-			$set: set
-		});
-
-		publishReversiGameStream(gameId, 'update-form', {
-			userId: user._id,
-			id,
-			value
-		});
-	}
-
-	async function message(message: any) {
-		message.id = Math.random();
-		publishReversiGameStream(gameId, 'message', {
-			userId: user._id,
-			message
-		});
-	}
-
-	async function accept(accept: boolean) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (game.isStarted) return;
-
-		let bothAccepted = false;
-
-		if (game.user1Id.equals(user._id)) {
-			await ReversiGame.update({ _id: gameId }, {
-				$set: {
-					user1Accepted: accept
-				}
-			});
-
-			publishReversiGameStream(gameId, 'change-accepts', {
-				user1: accept,
-				user2: game.user2Accepted
-			});
-
-			if (accept && game.user2Accepted) bothAccepted = true;
-		} else if (game.user2Id.equals(user._id)) {
-			await ReversiGame.update({ _id: gameId }, {
-				$set: {
-					user2Accepted: accept
-				}
-			});
-
-			publishReversiGameStream(gameId, 'change-accepts', {
-				user1: game.user1Accepted,
-				user2: accept
-			});
-
-			if (accept && game.user1Accepted) bothAccepted = true;
-		} else {
-			return;
-		}
-
-		if (bothAccepted) {
-			// 3秒後、まだacceptされていたらゲーム開始
-			setTimeout(async () => {
-				const freshGame = await ReversiGame.findOne({ _id: gameId });
-				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
-				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
-
-				let bw: number;
-				if (freshGame.settings.bw == 'random') {
-					bw = Math.random() > 0.5 ? 1 : 2;
-				} else {
-					bw = freshGame.settings.bw as number;
-				}
-
-				function getRandomMap() {
-					const mapCount = Object.entries(maps).length;
-					const rnd = Math.floor(Math.random() * mapCount);
-					return Object.values(maps)[rnd].data;
-				}
-
-				const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap();
-
-				await ReversiGame.update({ _id: gameId }, {
-					$set: {
-						startedAt: new Date(),
-						isStarted: true,
-						black: bw,
-						'settings.map': map
-					}
-				});
-
-				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
-				const o = new Reversi(map, {
-					isLlotheo: freshGame.settings.isLlotheo,
-					canPutEverywhere: freshGame.settings.canPutEverywhere,
-					loopedBoard: freshGame.settings.loopedBoard
-				});
-
-				if (o.isEnded) {
-					let winner;
-					if (o.winner === true) {
-						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
-					} else if (o.winner === false) {
-						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
-					} else {
-						winner = null;
-					}
-
-					await ReversiGame.update({
-						_id: gameId
-					}, {
-							$set: {
-								isEnded: true,
-								winnerId: winner
-							}
-						});
-
-					publishReversiGameStream(gameId, 'ended', {
-						winnerId: winner,
-						game: await pack(gameId, user)
-					});
-				}
-				//#endregion
-
-				publishReversiGameStream(gameId, 'started', await pack(gameId, user));
-			}, 3000);
-		}
-	}
-
-	// 石を打つ
-	async function set(pos: number) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (!game.isStarted) return;
-		if (game.isEnded) return;
-		if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) return;
-
-		const o = new Reversi(game.settings.map, {
-			isLlotheo: game.settings.isLlotheo,
-			canPutEverywhere: game.settings.canPutEverywhere,
-			loopedBoard: game.settings.loopedBoard
-		});
-
-		game.logs.forEach(log => {
-			o.put(log.color, log.pos);
-		});
-
-		const myColor =
-			(game.user1Id.equals(user._id) && game.black == 1) || (game.user2Id.equals(user._id) && game.black == 2)
-				? true
-				: false;
-
-		if (!o.canPut(myColor, pos)) return;
-		o.put(myColor, pos);
-
-		let winner;
-		if (o.isEnded) {
-			if (o.winner === true) {
-				winner = game.black == 1 ? game.user1Id : game.user2Id;
-			} else if (o.winner === false) {
-				winner = game.black == 1 ? game.user2Id : game.user1Id;
-			} else {
-				winner = null;
-			}
-		}
-
-		const log = {
-			at: new Date(),
-			color: myColor,
-			pos
-		};
-
-		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString());
-
-		await ReversiGame.update({
-			_id: gameId
-		}, {
-				$set: {
-					crc32,
-					isEnded: o.isEnded,
-					winnerId: winner
-				},
-				$push: {
-					logs: log
-				}
-			});
-
-		publishReversiGameStream(gameId, 'set', Object.assign(log, {
-			next: o.turn
-		}));
-
-		if (o.isEnded) {
-			publishReversiGameStream(gameId, 'ended', {
-				winnerId: winner,
-				game: await pack(gameId, user)
-			});
-		}
-	}
-
-	async function check(crc32: string) {
-		const game = await ReversiGame.findOne({ _id: gameId });
-
-		if (!game.isStarted) return;
-
-		// 互換性のため
-		if (game.crc32 == null) return;
-
-		if (crc32 !== game.crc32) {
-			connection.send(JSON.stringify({
-				type: 'rescue',
-				body: await pack(game, user)
-			}));
-		}
-	}
-}
diff --git a/src/server/api/stream/games/reversi.ts b/src/server/api/stream/games/reversi.ts
deleted file mode 100644
index f467613b21..0000000000
--- a/src/server/api/stream/games/reversi.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as mongo from 'mongodb';
-import * as websocket from 'websocket';
-import Xev from 'xev';
-import Matching, { pack } from '../../../../models/games/reversi/matching';
-import { publishUserStream } from '../../../../stream';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void {
-	// Subscribe reversi stream
-	subscriber.on(`reversi-stream:${user._id}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-
-	connection.on('message', async (data) => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'ping':
-				if (msg.id == null) return;
-				const matching = await Matching.findOne({
-					parentId: user._id,
-					childId: new mongo.ObjectID(msg.id)
-				});
-				if (matching == null) return;
-				publishUserStream(matching.childId, 'reversi_invited', await pack(matching, matching.childId));
-				break;
-		}
-	});
-}
diff --git a/src/server/api/stream/global-timeline.ts b/src/server/api/stream/global-timeline.ts
deleted file mode 100644
index 03852fb181..0000000000
--- a/src/server/api/stream/global-timeline.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-import { IUser } from '../../../models/user';
-import Mute from '../../../models/mute';
-import shouldMuteThisNote from '../../../misc/should-mute-this-note';
-
-export default async function(
-	request: websocket.request,
-	connection: websocket.connection,
-	subscriber: Xev,
-	user: IUser
-) {
-	const mute = await Mute.find({ muterId: user._id });
-	const mutedUserIds = mute.map(m => m.muteeId.toString());
-
-	// Subscribe stream
-	subscriber.on('global-timeline', async note => {
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, mutedUserIds)) return;
-
-		connection.send(JSON.stringify({
-			type: 'note',
-			body: note
-		}));
-	});
-}
diff --git a/src/server/api/stream/hashtag.ts b/src/server/api/stream/hashtag.ts
deleted file mode 100644
index 54da4f9ad9..0000000000
--- a/src/server/api/stream/hashtag.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-import { IUser } from '../../../models/user';
-import Mute from '../../../models/mute';
-import { pack } from '../../../models/note';
-import shouldMuteThisNote from '../../../misc/should-mute-this-note';
-
-export default async function(
-	request: websocket.request,
-	connection: websocket.connection,
-	subscriber: Xev,
-	user?: IUser
-) {
-	const mute = user ? await Mute.find({ muterId: user._id }) : null;
-	const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
-
-	const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q);
-
-	// Subscribe stream
-	subscriber.on('hashtag', async note => {
-		const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
-		if (!matched) return;
-
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, mutedUserIds)) return;
-
-		connection.send(JSON.stringify({
-			type: 'note',
-			body: note
-		}));
-	});
-}
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
deleted file mode 100644
index 5575d0d523..0000000000
--- a/src/server/api/stream/home.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-import * as debug from 'debug';
-
-import User, { IUser } from '../../../models/user';
-import Mute from '../../../models/mute';
-import { pack as packNote, pack } from '../../../models/note';
-import readNotification from '../common/read-notification';
-import call from '../call';
-import { IApp } from '../../../models/app';
-import shouldMuteThisNote from '../../../misc/should-mute-this-note';
-import readNote from '../../../services/note/read';
-
-const log = debug('misskey');
-
-export default async function(
-	request: websocket.request,
-	connection: websocket.connection,
-	subscriber: Xev,
-	user: IUser,
-	app: IApp
-) {
-	const mute = await Mute.find({ muterId: user._id });
-	const mutedUserIds = mute.map(m => m.muteeId.toString());
-
-	async function onNoteStream(noteId: any) {
-		const note = await packNote(noteId, user, {
-			detail: true
-		});
-
-		connection.send(JSON.stringify({
-			type: 'note-updated',
-			body: {
-				note: note
-			}
-		}));
-	}
-
-	// Subscribe Home stream channel
-	subscriber.on(`user-stream:${user._id}`, async x => {
-		// Renoteなら再pack
-		if (x.type == 'note' && x.body.renoteId != null) {
-			x.body.renote = await pack(x.body.renoteId, user, {
-				detail: true
-			});
-		}
-
-		//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
-		if (x.type == 'note') {
-			if (shouldMuteThisNote(x.body, mutedUserIds)) return;
-		} else if (x.type == 'notification') {
-			if (mutedUserIds.includes(x.body.userId)) {
-				return;
-			}
-		}
-		//#endregion
-
-		connection.send(JSON.stringify(x));
-	});
-
-	connection.on('message', async data => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'api':
-				// 新鮮なデータを利用するためにユーザーをフェッチ
-				call(msg.endpoint, await User.findOne({ _id: user._id }), app, msg.data).then(res => {
-					connection.send(JSON.stringify({
-						type: `api-res:${msg.id}`,
-						body: { res }
-					}));
-				}).catch(e => {
-					connection.send(JSON.stringify({
-						type: `api-res:${msg.id}`,
-						body: { e }
-					}));
-				});
-				break;
-
-			case 'alive':
-				// Update lastUsedAt
-				User.update({ _id: user._id }, {
-					$set: {
-						'lastUsedAt': new Date()
-					}
-				});
-				break;
-
-			case 'read_notification':
-				if (!msg.id) return;
-				readNotification(user._id, msg.id);
-				break;
-
-			case 'capture':
-				if (!msg.id) return;
-				log(`CAPTURE: ${msg.id} by @${user.username}`);
-				subscriber.on(`note-stream:${msg.id}`, onNoteStream);
-				if (msg.read) {
-					readNote(user._id, msg.id);
-				}
-				break;
-
-			case 'decapture':
-				if (!msg.id) return;
-				log(`DECAPTURE: ${msg.id} by @${user.username}`);
-				subscriber.off(`note-stream:${msg.id}`, onNoteStream);
-				break;
-		}
-	});
-}
diff --git a/src/server/api/stream/hybrid-timeline.ts b/src/server/api/stream/hybrid-timeline.ts
deleted file mode 100644
index 045b822783..0000000000
--- a/src/server/api/stream/hybrid-timeline.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-import { IUser } from '../../../models/user';
-import Mute from '../../../models/mute';
-import { pack } from '../../../models/note';
-import shouldMuteThisNote from '../../../misc/should-mute-this-note';
-
-export default async function(
-	request: websocket.request,
-	connection: websocket.connection,
-	subscriber: Xev,
-	user: IUser
-) {
-	const mute = await Mute.find({ muterId: user._id });
-	const mutedUserIds = mute.map(m => m.muteeId.toString());
-
-	// Subscribe stream
-	subscriber.on('hybrid-timeline', onEvent);
-	subscriber.on(`hybrid-timeline:${user._id}`, onEvent);
-
-	async function onEvent(note: any) {
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, mutedUserIds)) return;
-
-		connection.send(JSON.stringify({
-			type: 'note',
-			body: note
-		}));
-	}
-}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
new file mode 100644
index 0000000000..bd99f2755e
--- /dev/null
+++ b/src/server/api/stream/index.ts
@@ -0,0 +1,213 @@
+import autobind from 'autobind-decorator';
+import * as websocket from 'websocket';
+import Xev from 'xev';
+import * as debug from 'debug';
+
+import User, { IUser } from '../../../models/user';
+import readNotification from '../common/read-notification';
+import call from '../call';
+import { IApp } from '../../../models/app';
+import readNote from '../../../services/note/read';
+
+import Channel from './channel';
+import channels from './channels';
+
+const log = debug('misskey');
+
+/**
+ * Main stream connection
+ */
+export default class Connection {
+	public user?: IUser;
+	public app: IApp;
+	private wsConnection: websocket.connection;
+	public subscriber: Xev;
+	private channels: Channel[] = [];
+	private subscribingNotes: any = {};
+
+	constructor(
+		wsConnection: websocket.connection,
+		subscriber: Xev,
+		user: IUser,
+		app: IApp
+	) {
+		this.wsConnection = wsConnection;
+		this.user = user;
+		this.app = app;
+		this.subscriber = subscriber;
+
+		this.wsConnection.on('message', this.onWsConnectionMessage);
+	}
+
+	/**
+	 * クライアントからメッセージ受信時
+	 */
+	@autobind
+	private async onWsConnectionMessage(data: websocket.IMessage) {
+		const { type, body } = JSON.parse(data.utf8Data);
+
+		switch (type) {
+			case 'api': this.onApiRequest(body); break;
+			case 'alive': this.onAlive(); break;
+			case 'readNotification': this.onReadNotification(body); break;
+			case 'subNote': this.onSubscribeNote(body); break;
+			case 'sn': this.onSubscribeNote(body); break; // alias
+			case 'unsubNote': this.onUnsubscribeNote(body); break;
+			case 'un': this.onUnsubscribeNote(body); break; // alias
+			case 'connect': this.onChannelConnectRequested(body); break;
+			case 'disconnect': this.onChannelDisconnectRequested(body); break;
+			case 'channel': this.onChannelMessageRequested(body); break;
+		}
+	}
+
+	/**
+	 * APIリクエスト要求時
+	 */
+	@autobind
+	private async onApiRequest(payload: any) {
+		// 新鮮なデータを利用するためにユーザーをフェッチ
+		const user = this.user ? await User.findOne({ _id: this.user._id }) : null;
+
+		const endpoint = payload.endpoint || payload.ep; // alias
+
+		// 呼び出し
+		call(endpoint, user, this.app, payload.data).then(res => {
+			this.sendMessageToWs(`api:${payload.id}`, { res });
+		}).catch(e => {
+			this.sendMessageToWs(`api:${payload.id}`, { e });
+		});
+	}
+
+	@autobind
+	private onAlive() {
+		// Update lastUsedAt
+		User.update({ _id: this.user._id }, {
+			$set: {
+				'lastUsedAt': new Date()
+			}
+		});
+	}
+
+	@autobind
+	private onReadNotification(payload: any) {
+		if (!payload.id) return;
+		readNotification(this.user._id, payload.id);
+	}
+
+	/**
+	 * 投稿購読要求時
+	 */
+	@autobind
+	private onSubscribeNote(payload: any) {
+		if (!payload.id) return;
+
+		if (this.subscribingNotes[payload.id] == null) {
+			this.subscribingNotes[payload.id] = 0;
+		}
+
+		this.subscribingNotes[payload.id]++;
+
+		if (this.subscribingNotes[payload.id] == 1) {
+			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
+		}
+
+		if (payload.read) {
+			readNote(this.user._id, payload.id);
+		}
+	}
+
+	/**
+	 * 投稿購読解除要求時
+	 */
+	@autobind
+	private onUnsubscribeNote(payload: any) {
+		if (!payload.id) return;
+
+		this.subscribingNotes[payload.id]--;
+		if (this.subscribingNotes[payload.id] <= 0) {
+			delete this.subscribingNotes[payload.id];
+			this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
+		}
+	}
+
+	@autobind
+	private async onNoteStreamMessage(data: any) {
+		this.sendMessageToWs('noteUpdated', {
+			id: data.body.id,
+			type: data.type,
+			body: data.body.body,
+		});
+	}
+
+	/**
+	 * チャンネル接続要求時
+	 */
+	@autobind
+	private onChannelConnectRequested(payload: any) {
+		const { channel, id, params } = payload;
+		log(`CH CONNECT: ${id} ${channel} by @${this.user.username}`);
+		this.connectChannel(id, params, (channels as any)[channel]);
+	}
+
+	/**
+	 * チャンネル切断要求時
+	 */
+	@autobind
+	private onChannelDisconnectRequested(payload: any) {
+		const { id } = payload;
+		log(`CH DISCONNECT: ${id} by @${this.user.username}`);
+		this.disconnectChannel(id);
+	}
+
+	/**
+	 * クライアントにメッセージ送信
+	 */
+	@autobind
+	public sendMessageToWs(type: string, payload: any) {
+		this.wsConnection.send(JSON.stringify({
+			type: type,
+			body: payload
+		}));
+	}
+
+	/**
+	 * チャンネルに接続
+	 */
+	@autobind
+	private connectChannel(id: string, params: any, channelClass: { new(id: string, connection: Connection): Channel }) {
+		const channel = new channelClass(id, this);
+		this.channels.push(channel);
+		channel.init(params);
+	}
+
+	/**
+	 * チャンネルから切断
+	 */
+	@autobind
+	private disconnectChannel(id: string) {
+		const channel = this.channels.find(c => c.id === id);
+
+		if (channel) {
+			if (channel.dispose) channel.dispose();
+			this.channels = this.channels.filter(c => c.id !== id);
+		}
+	}
+
+	@autobind
+	private onChannelMessageRequested(data: any) {
+		const channel = this.channels.find(c => c.id === data.id);
+		if (channel != null && channel.onMessage != null) {
+			channel.onMessage(data.type, data.body);
+		}
+	}
+
+	/**
+	 * ストリームが切れたとき
+	 */
+	@autobind
+	public dispose() {
+		this.channels.forEach(c => {
+			if (c.dispose) c.dispose();
+		});
+	}
+}
diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts
deleted file mode 100644
index ae054a5f9f..0000000000
--- a/src/server/api/stream/local-timeline.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-import { IUser } from '../../../models/user';
-import Mute from '../../../models/mute';
-import { pack } from '../../../models/note';
-import shouldMuteThisNote from '../../../misc/should-mute-this-note';
-
-export default async function(
-	request: websocket.request,
-	connection: websocket.connection,
-	subscriber: Xev,
-	user?: IUser
-) {
-	const mute = user ? await Mute.find({ muterId: user._id }) : null;
-	const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
-
-	// Subscribe stream
-	subscriber.on('local-timeline', async note => {
-		// Renoteなら再pack
-		if (note.renoteId != null) {
-			note.renote = await pack(note.renoteId, user, {
-				detail: true
-			});
-		}
-
-		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, mutedUserIds)) return;
-
-		connection.send(JSON.stringify({
-			type: 'note',
-			body: note
-		}));
-	});
-}
diff --git a/src/server/api/stream/messaging-index.ts b/src/server/api/stream/messaging-index.ts
deleted file mode 100644
index 9af63f2812..0000000000
--- a/src/server/api/stream/messaging-index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void {
-	// Subscribe messaging index stream
-	subscriber.on(`messaging-index-stream:${user._id}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-}
diff --git a/src/server/api/stream/messaging.ts b/src/server/api/stream/messaging.ts
deleted file mode 100644
index 8b352cea3c..0000000000
--- a/src/server/api/stream/messaging.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-import read from '../common/read-messaging-message';
-import { ParsedUrlQuery } from 'querystring';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void {
-	const q = request.resourceURL.query as ParsedUrlQuery;
-	const otherparty = q.otherparty as string;
-
-	// Subscribe messaging stream
-	subscriber.on(`messaging-stream:${user._id}-${otherparty}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-
-	connection.on('message', async (data) => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'read':
-				if (!msg.id) return;
-				read(user._id, otherparty, msg.id);
-				break;
-		}
-	});
-}
diff --git a/src/server/api/stream/notes-stats.ts b/src/server/api/stream/notes-stats.ts
deleted file mode 100644
index ba99403226..0000000000
--- a/src/server/api/stream/notes-stats.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-const ev = new Xev();
-
-export default function(request: websocket.request, connection: websocket.connection): void {
-	const onStats = (stats: any) => {
-		connection.send(JSON.stringify({
-			type: 'stats',
-			body: stats
-		}));
-	};
-
-	connection.on('message', async data => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'requestLog':
-				ev.once(`notesStatsLog:${msg.id}`, statsLog => {
-					connection.send(JSON.stringify({
-						type: 'statsLog',
-						body: statsLog
-					}));
-				});
-				ev.emit('requestNotesStatsLog', msg.id);
-				break;
-		}
-	});
-
-	ev.addListener('notesStats', onStats);
-
-	connection.on('close', () => {
-		ev.removeListener('notesStats', onStats);
-	});
-}
diff --git a/src/server/api/stream/server-stats.ts b/src/server/api/stream/server-stats.ts
deleted file mode 100644
index d4fbeefa04..0000000000
--- a/src/server/api/stream/server-stats.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-
-const ev = new Xev();
-
-export default function(request: websocket.request, connection: websocket.connection): void {
-	const onStats = (stats: any) => {
-		connection.send(JSON.stringify({
-			type: 'stats',
-			body: stats
-		}));
-	};
-
-	connection.on('message', async data => {
-		const msg = JSON.parse(data.utf8Data);
-
-		switch (msg.type) {
-			case 'requestLog':
-				ev.once(`serverStatsLog:${msg.id}`, statsLog => {
-					connection.send(JSON.stringify({
-						type: 'statsLog',
-						body: statsLog
-					}));
-				});
-				ev.emit('requestServerStatsLog', {
-					id: msg.id,
-					length: msg.length
-				});
-				break;
-		}
-	});
-
-	ev.addListener('serverStats', onStats);
-
-	connection.on('close', () => {
-		ev.removeListener('serverStats', onStats);
-	});
-}
diff --git a/src/server/api/stream/user-list.ts b/src/server/api/stream/user-list.ts
deleted file mode 100644
index 30f94d5251..0000000000
--- a/src/server/api/stream/user-list.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as websocket from 'websocket';
-import Xev from 'xev';
-import { ParsedUrlQuery } from 'querystring';
-
-export default function(request: websocket.request, connection: websocket.connection, subscriber: Xev, user: any): void {
-	const q = request.resourceURL.query as ParsedUrlQuery;
-	const listId = q.listId as string;
-
-	// Subscribe stream
-	subscriber.on(`user-list-stream:${listId}`, data => {
-		connection.send(JSON.stringify(data));
-	});
-}
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index 09ec23a743..4518d21c3f 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -2,26 +2,12 @@ import * as http from 'http';
 import * as websocket from 'websocket';
 import Xev from 'xev';
 
-import homeStream from './stream/home';
-import localTimelineStream from './stream/local-timeline';
-import hybridTimelineStream from './stream/hybrid-timeline';
-import globalTimelineStream from './stream/global-timeline';
-import userListStream from './stream/user-list';
-import driveStream from './stream/drive';
-import messagingStream from './stream/messaging';
-import messagingIndexStream from './stream/messaging-index';
-import reversiGameStream from './stream/games/reversi-game';
-import reversiStream from './stream/games/reversi';
-import serverStatsStream from './stream/server-stats';
-import notesStatsStream from './stream/notes-stats';
-import hashtagStream from './stream/hashtag';
+import MainStreamConnection from './stream';
 import { ParsedUrlQuery } from 'querystring';
 import authenticate from './authenticate';
 
 module.exports = (server: http.Server) => {
-	/**
-	 * Init websocket server
-	 */
+	// Init websocket server
 	const ws = new websocket.server({
 		httpServer: server
 	});
@@ -29,20 +15,16 @@ module.exports = (server: http.Server) => {
 	ws.on('request', async (request) => {
 		const connection = request.accept();
 
-		if (request.resourceURL.pathname === '/server-stats') {
-			serverStatsStream(request, connection);
-			return;
-		}
-
-		if (request.resourceURL.pathname === '/notes-stats') {
-			notesStatsStream(request, connection);
-			return;
-		}
-
 		const ev = new Xev();
 
+		const q = request.resourceURL.query as ParsedUrlQuery;
+		const [user, app] = await authenticate(q.i as string);
+
+		const main = new MainStreamConnection(connection, ev, user, app);
+
 		connection.once('close', () => {
 			ev.removeAllListeners();
+			main.dispose();
 		});
 
 		connection.on('message', async (data) => {
@@ -50,46 +32,5 @@ module.exports = (server: http.Server) => {
 				connection.send('pong');
 			}
 		});
-
-		const q = request.resourceURL.query as ParsedUrlQuery;
-		const [user, app] = await authenticate(q.i as string);
-
-		if (request.resourceURL.pathname === '/games/reversi-game') {
-			reversiGameStream(request, connection, ev, user);
-			return;
-		}
-
-		if (request.resourceURL.pathname === '/local-timeline') {
-			localTimelineStream(request, connection, ev, user);
-			return;
-		}
-
-		if (request.resourceURL.pathname === '/hashtag') {
-			hashtagStream(request, connection, ev, user);
-			return;
-		}
-
-		if (user == null) {
-			connection.send('authentication-failed');
-			connection.close();
-			return;
-		}
-
-		const channel: any =
-			request.resourceURL.pathname === '/' ? homeStream :
-			request.resourceURL.pathname === '/hybrid-timeline' ? hybridTimelineStream :
-			request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream :
-			request.resourceURL.pathname === '/user-list' ? userListStream :
-			request.resourceURL.pathname === '/drive' ? driveStream :
-			request.resourceURL.pathname === '/messaging' ? messagingStream :
-			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
-			request.resourceURL.pathname === '/games/reversi' ? reversiStream :
-			null;
-
-		if (channel !== null) {
-			channel(request, connection, ev, user, app);
-		} else {
-			connection.close();
-		}
 	});
 };
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 666a6ca742..f8c54b2af4 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -12,7 +12,7 @@ import * as sharp from 'sharp';
 import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
 import DriveFolder from '../../models/drive-folder';
 import { pack } from '../../models/drive-file';
-import { publishUserStream, publishDriveStream } from '../../stream';
+import { publishMainStream, publishDriveStream } from '../../stream';
 import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 import delFile from './delete-file';
 import config from '../../config';
@@ -383,8 +383,8 @@ export default async function(
 	log(`drive file has been created ${driveFile._id}`);
 
 	pack(driveFile).then(packedFile => {
-		// Publish drive_file_created event
-		publishUserStream(user._id, 'drive_file_created', packedFile);
+		// Publish driveFileCreated event
+		publishMainStream(user._id, 'driveFileCreated', packedFile);
 		publishDriveStream(user._id, 'file_created', packedFile);
 	});
 
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index dd2fa544dc..637e3e8093 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -2,7 +2,7 @@ import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
-import { publishUserStream } from '../../stream';
+import { publishMainStream } from '../../stream';
 import notify from '../../notify';
 import pack from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
@@ -61,12 +61,12 @@ export default async function(follower: IUser, followee: IUser) {
 
 		// Publish follow event
 		if (isLocalUser(follower)) {
-			packUser(followee, follower).then(packed => publishUserStream(follower._id, 'follow', packed));
+			packUser(followee, follower).then(packed => publishMainStream(follower._id, 'follow', packed));
 		}
 
 		// Publish followed event
 		if (isLocalUser(followee)) {
-			packUser(follower, followee).then(packed => publishUserStream(followee._id, 'followed', packed)),
+			packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)),
 
 			// 通知を作成
 			notify(followee._id, follower._id, 'follow');
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index 7c285e9eac..2a67acbf05 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -2,7 +2,7 @@ import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../
 import Following from '../../models/following';
 import FollowingLog from '../../models/following-log';
 import FollowedLog from '../../models/followed-log';
-import { publishUserStream } from '../../stream';
+import { publishMainStream } from '../../stream';
 import pack from '../../remote/activitypub/renderer';
 import renderFollow from '../../remote/activitypub/renderer/follow';
 import renderUndo from '../../remote/activitypub/renderer/undo';
@@ -52,7 +52,7 @@ export default async function(follower: IUser, followee: IUser) {
 
 	// Publish unfollow event
 	if (isLocalUser(follower)) {
-		packUser(followee, follower).then(packed => publishUserStream(follower._id, 'unfollow', packed));
+		packUser(followee, follower).then(packed => publishMainStream(follower._id, 'unfollow', packed));
 	}
 
 	if (isLocalUser(follower) && isRemoteUser(followee)) {
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
index 5e38879a49..e7c8df844a 100644
--- a/src/services/following/requests/accept.ts
+++ b/src/services/following/requests/accept.ts
@@ -7,7 +7,7 @@ import { deliver } from '../../../queue';
 import Following from '../../../models/following';
 import FollowingLog from '../../../models/following-log';
 import FollowedLog from '../../../models/followed-log';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 
 export default async function(followee: IUser, follower: IUser) {
 	const following = await Following.insert({
@@ -74,7 +74,7 @@ export default async function(followee: IUser, follower: IUser) {
 
 	packUser(followee, followee, {
 		detail: true
-	}).then(packed => publishUserStream(followee._id, 'meUpdated', packed));
+	}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
 
-	packUser(followee, follower).then(packed => publishUserStream(follower._id, 'follow', packed));
+	packUser(followee, follower).then(packed => publishMainStream(follower._id, 'follow', packed));
 }
diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts
index 9655a95f04..def02d59d9 100644
--- a/src/services/following/requests/cancel.ts
+++ b/src/services/following/requests/cancel.ts
@@ -4,7 +4,7 @@ import pack from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderUndo from '../../../remote/activitypub/renderer/undo';
 import { deliver } from '../../../queue';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 
 export default async function(followee: IUser, follower: IUser) {
 	if (isRemoteUser(followee)) {
@@ -34,5 +34,5 @@ export default async function(followee: IUser, follower: IUser) {
 
 	packUser(followee, followee, {
 		detail: true
-	}).then(packed => publishUserStream(followee._id, 'meUpdated', packed));
+	}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
 }
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
index 946c22568c..5e613fd053 100644
--- a/src/services/following/requests/create.ts
+++ b/src/services/following/requests/create.ts
@@ -1,5 +1,5 @@
 import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 import notify from '../../../notify';
 import pack from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
@@ -33,11 +33,11 @@ export default async function(follower: IUser, followee: IUser) {
 
 	// Publish receiveRequest event
 	if (isLocalUser(followee)) {
-		packUser(follower, followee).then(packed => publishUserStream(followee._id, 'receiveFollowRequest', packed));
+		packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed));
 
 		packUser(followee, followee, {
 			detail: true
-		}).then(packed => publishUserStream(followee._id, 'meUpdated', packed));
+		}).then(packed => publishMainStream(followee._id, 'meUpdated', packed));
 
 		// 通知を作成
 		notify(followee._id, follower._id, 'receiveFollowRequest');
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
index eda6716321..91a49db997 100644
--- a/src/services/following/requests/reject.ts
+++ b/src/services/following/requests/reject.ts
@@ -4,7 +4,7 @@ import pack from '../../../remote/activitypub/renderer';
 import renderFollow from '../../../remote/activitypub/renderer/follow';
 import renderReject from '../../../remote/activitypub/renderer/reject';
 import { deliver } from '../../../queue';
-import { publishUserStream } from '../../../stream';
+import { publishMainStream } from '../../../stream';
 
 export default async function(followee: IUser, follower: IUser) {
 	if (isRemoteUser(follower)) {
@@ -23,5 +23,5 @@ export default async function(followee: IUser, follower: IUser) {
 		}
 	});
 
-	packUser(followee, follower).then(packed => publishUserStream(follower._id, 'unfollow', packed));
+	packUser(followee, follower).then(packed => publishMainStream(follower._id, 'unfollow', packed));
 }
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 2f2ac18e9b..3dc411d434 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,7 +1,7 @@
 import es from '../../db/elasticsearch';
 import Note, { pack, INote } from '../../models/note';
 import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
-import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream';
+import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream';
 import Following from '../../models/following';
 import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
@@ -234,7 +234,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 		// 通知
 		if (isLocalUser(data.reply._user)) {
 			nm.push(data.reply.userId, 'reply');
-			publishUserStream(data.reply.userId, 'reply', noteObj);
+			publishMainStream(data.reply.userId, 'reply', noteObj);
 		}
 	}
 
@@ -257,7 +257,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 
 		// Publish event
 		if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) {
-			publishUserStream(data.renote.userId, 'renote', noteObj);
+			publishMainStream(data.renote.userId, 'renote', noteObj);
 		}
 	}
 
@@ -306,11 +306,11 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
 				detail: true
 			});
 			// Publish event to myself's stream
-			publishUserStream(note.userId, 'note', detailPackedNote);
+			publishHomeTimelineStream(note.userId, detailPackedNote);
 			publishHybridTimelineStream(note.userId, detailPackedNote);
 		} else {
 			// Publish event to myself's stream
-			publishUserStream(note.userId, 'note', noteObj);
+			publishHomeTimelineStream(note.userId, noteObj);
 
 			// Publish note to local and hybrid timeline stream
 			if (note.visibility != 'home') {
@@ -495,7 +495,7 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
 			}
 
 			// Publish event to followers stream
-			publishUserStream(following.followerId, 'note', detailPackedNote);
+			publishHomeTimelineStream(following.followerId, detailPackedNote);
 
 			if (isRemoteUser(user) || note.visibility != 'public') {
 				publishHybridTimelineStream(following.followerId, detailPackedNote);
@@ -526,7 +526,7 @@ function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: Notific
 			detail: true
 		});
 
-		publishUserStream(u._id, 'mention', detailPackedNote);
+		publishMainStream(u._id, 'mention', detailPackedNote);
 
 		// Create notification
 		nm.push(u._id, 'mention');
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index b164d59781..b5cf42ec28 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -30,7 +30,7 @@ export default async function(user: IUser, note: INote) {
 		}
 	});
 
-	publishNoteStream(note._id, 'deleted');
+	publishNoteStream(note._id, 'deleted', {});
 
 	//#region ローカルの投稿なら削除アクティビティを配送
 	if (isLocalUser(user)) {
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index 5b6267b0dd..6884014e33 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -43,7 +43,9 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
 		$inc: inc
 	});
 
-	publishNoteStream(note._id, 'reacted');
+	publishNoteStream(note._id, 'reacted', {
+		reaction: reaction
+	});
 
 	// リアクションされたユーザーがローカルユーザーなら通知を作成
 	if (isLocalUser(note._user)) {
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 8622597bc0..caf5cf318f 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -1,5 +1,5 @@
 import * as mongo from 'mongodb';
-import { publishUserStream } from '../../stream';
+import { publishMainStream } from '../../stream';
 import User from '../../models/user';
 import NoteUnread from '../../models/note-unread';
 
@@ -56,11 +56,11 @@ export default (
 
 	if (count1 == 0) {
 		// 全て既読になったイベントを発行
-		publishUserStream(userId, 'readAllUnreadMentions');
+		publishMainStream(userId, 'readAllUnreadMentions');
 	}
 
 	if (count2 == 0) {
 		// 全て既読になったイベントを発行
-		publishUserStream(userId, 'readAllUnreadSpecifiedNotes');
+		publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
 	}
 });
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index 2d50976283..5953f61fe1 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -2,7 +2,7 @@ import NoteUnread from '../../models/note-unread';
 import User, { IUser } from '../../models/user';
 import { INote } from '../../models/note';
 import Mute from '../../models/mute';
-import { publishUserStream } from '../../stream';
+import { publishMainStream } from '../../stream';
 
 export default async function(user: IUser, note: INote, isSpecified = false) {
 	//#region ミュートしているなら無視
@@ -38,10 +38,10 @@ export default async function(user: IUser, note: INote, isSpecified = false) {
 			}
 		});
 
-		publishUserStream(user._id, 'unreadMention', note._id);
+		publishMainStream(user._id, 'unreadMention', note._id);
 
 		if (isSpecified) {
-			publishUserStream(user._id, 'unreadSpecifiedNote', note._id);
+			publishMainStream(user._id, 'unreadSpecifiedNote', note._id);
 		}
 	}, 3000);
 }
diff --git a/src/stream.ts b/src/stream.ts
index 8a8d8b4cf0..4cd8fc8b2c 100644
--- a/src/stream.ts
+++ b/src/stream.ts
@@ -31,52 +31,59 @@ class Publisher {
 		this.ev.emit(channel, message);
 	}
 
-	public publishUserStream = (userId: ID, type: string, value?: any): void => {
-		this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+	public publishMainStream = (userId: ID, type: string, value?: any): void => {
+		this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishDriveStream = (userId: ID, type: string, value?: any): void => {
-		this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
-	public publishNoteStream = (noteId: ID, type: string): void => {
-		this.publish(`note-stream:${noteId}`, null, noteId);
+	public publishNoteStream = (noteId: ID, type: string, value: any): void => {
+		this.publish(`noteStream:${noteId}`, type, {
+			id: noteId,
+			body: value
+		});
 	}
 
 	public publishUserListStream = (listId: ID, type: string, value?: any): void => {
-		this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
-		this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
-		this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishReversiStream = (userId: ID, type: string, value?: any): void => {
-		this.publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
 	public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
-		this.publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+	}
+
+	public publishHomeTimelineStream = (userId: ID, note: any): void => {
+		this.publish(`homeTimeline:${userId}`, null, note);
 	}
 
 	public publishLocalTimelineStream = async (note: any): Promise<void> => {
 		const meta = await this.getMeta();
 		if (meta.disableLocalTimeline) return;
-		this.publish('local-timeline', null, note);
+		this.publish('localTimeline', null, note);
 	}
 
 	public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
 		const meta = await this.getMeta();
 		if (meta.disableLocalTimeline) return;
-		this.publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
+		this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note);
 	}
 
 	public publishGlobalTimelineStream = (note: any): void => {
-		this.publish('global-timeline', null, note);
+		this.publish('globalTimeline', null, note);
 	}
 
 	public publishHashtagStream = (note: any): void => {
@@ -88,7 +95,7 @@ const publisher = new Publisher();
 
 export default publisher;
 
-export const publishUserStream = publisher.publishUserStream;
+export const publishMainStream = publisher.publishMainStream;
 export const publishDriveStream = publisher.publishDriveStream;
 export const publishNoteStream = publisher.publishNoteStream;
 export const publishUserListStream = publisher.publishUserListStream;
@@ -96,6 +103,7 @@ export const publishMessagingStream = publisher.publishMessagingStream;
 export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
 export const publishReversiStream = publisher.publishReversiStream;
 export const publishReversiGameStream = publisher.publishReversiGameStream;
+export const publishHomeTimelineStream = publisher.publishHomeTimelineStream;
 export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
 export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
 export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
diff --git a/tsconfig.json b/tsconfig.json
index 76221c282a..ff4eaf917a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,7 +14,8 @@
     "removeComments": false,
     "noLib": false,
     "strict": true,
-    "strictNullChecks": false
+    "strictNullChecks": false,
+    "experimentalDecorators": true
   },
   "compileOnSave": false,
   "include": [