diff --git a/.travis.yml b/.travis.yml
index e1481ee..7ee2f12 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,13 +19,13 @@ before_deploy:
   - export DISTDIR=/tmp/dist
   - git fetch origin gh-pages:gh-pages
   - git worktree add ${DISTDIR} gh-pages
-  - cp ./build-web/dan2-web.js ${DISTDIR}/dan2.js
-  - cp ./build-web/dan2-web.js ${DISTDIR}/dan2-${TRAVIS_COMMIT}.js
+  - cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs.js
+  - cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs-${TRAVIS_COMMIT}.js
   - echo "TRAVIS_TAG = ${TRAVIS_TAG}"
   - echo "TRAVIS_BRANCH = ${TRAVIS_BRANCH}"
   - if [ "${TRAVIS_TAG}" ]; then
       echo 'Add tags build';
-      cp ./build-web/dan2-web.js ${DISTDIR}/dan2-${TRAVIS_TAG}.js;
+      cp ./build-web/iottalkjs-web.js ${DISTDIR}/iottalkjs-${TRAVIS_TAG}.js;
     fi
 
 deploy:
diff --git a/examples/Dummy_Device/index.html b/examples/Dummy_Device/index.html
new file mode 100644
index 0000000..c2be1e5
--- /dev/null
+++ b/examples/Dummy_Device/index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <title>Dummy_Device</title>
+  <script src="https://code.jquery.com/jquery-3.4.1.min.js"
+    integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
+  <script src="js/sa.js"></script>
+  <script src="../../build-web/iottalkjs-web.js"></script>
+
+</head>
+
+<body>
+  <legend>Dummy_Sensor</legend>
+  <legend class='IDF_value'>_____________</legend>
+  <legend>Dummy_Control</legend>
+  <legend class='ODF_value'>_____________</legend>
+</body>
+
+</html>
diff --git a/examples/Dummy_Device/js/sa.js b/examples/Dummy_Device/js/sa.js
new file mode 100644
index 0000000..cc87ea8
--- /dev/null
+++ b/examples/Dummy_Device/js/sa.js
@@ -0,0 +1,27 @@
+$(function () {
+    function Dummy_Sensor() {
+        var number = Math.floor((1 + Math.random()) * 0x10000);
+        $('.IDF_value')[0].innerText = number;
+        return [number];
+    }
+
+    function Dummy_Control(data) {
+        $('.ODF_value')[0].innerText = data[0];
+    }
+
+    var option = {
+        'api_url': 'https://iottalk2.tw/csm',
+        'device_model': 'Dummy_Device',
+        'device_addr': 'c96ca71c-9e48-2a23-2868-acb420a2f105',
+        'device_name': 'Dummy',
+        'persistent_binding': true,
+        'idf_list': [[Dummy_Sensor, ['int']]],
+        'odf_list': [Dummy_Control],
+        'push_interval': 0,
+        'interval': {
+            'Dummy_Sensor': 1.5,
+        }
+    };
+
+    new iottalkjs.dai(option).run();
+});
diff --git a/package.json b/package.json
index da1e160..ea063b2 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,14 @@
   "name": "iottalk-js",
   "version": "2.0.4",
   "description": "IoTtalk javascript library SDK",
-  "main": "dan2.js",
+  "main": "iottalkjs.js",
   "directories": {
     "example": "examples"
   },
   "scripts": {
     "start": "npx webpack -w --mode development",
     "build": "npm run build:node && npm run build:web",
-    "build:node": "npx babel src --out-dir build-node && npx ncc build build-node/dan2.js -o build-node/dist && mv build-node/dist/index.js build-node/dist/dan2.js",
+    "build:node": "npx babel src --out-dir build-node && npx ncc build build-node/index.js -o build-node/dist && mv build-node/dist/index.js build-node/dist/iottalkjs.js",
     "build:web": "npx webpack",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
diff --git a/src/context.js b/src/context.js
new file mode 100644
index 0000000..bb3d692
--- /dev/null
+++ b/src/context.js
@@ -0,0 +1,24 @@
+import ChannelPool from './channel-pool.js';
+
+export default class {
+    
+    constructor() {
+        this.url = null;
+        this.app_id = null;
+        this.name = null;
+        this.mqtt_host = null;
+        this.mqtt_port = null;
+        this.mqtt_username = null;
+        this.mqtt_password = null;
+        this.mqtt_client = null;
+        this.i_chans = new ChannelPool();
+        this.o_chans = new ChannelPool();
+        this.rev = null;
+        this.on_signal = null;
+        this.on_data = null;
+        this.on_register = null;
+        this.on_deregister = null;
+        this.on_connect = null;
+        this.on_disconnect = null;
+    }
+}
diff --git a/src/dai.js b/src/dai.js
new file mode 100644
index 0000000..5753d36
--- /dev/null
+++ b/src/dai.js
@@ -0,0 +1,203 @@
+import DeviceFeature from './device-feature.js';
+import { Client } from './dan.js';
+import { RegistrationError, ArgumentError } from './exceptions.js';
+
+export default class {
+
+    constructor(option) {
+        this.api_url = option['api_url'];
+        this.device_model = option['device_model'];
+        this.device_addr = option['device_addr'];
+        this.device_name = option['device_name'];
+        this.persistent_binding = option['persistent_binding'] || false;
+        this.username = option['username'];
+        this.extra_setup_webpage = option['extra_setup_webpage'] || '';
+        this.device_webpage = option['device_webpage'] || '';
+
+        this.register_callback = option['register_callback'];
+        this.on_register = option['on_register'];
+        this.on_deregister = option['on_deregister'];
+        this.on_connect = option['on_connect'];
+        this.on_disconnect = option['on_disconnect'];
+
+        this.push_interval = option['push_interval'] != undefined ? option['push_interval'] : 1;
+        this.interval = option['interval'] || {};
+
+        this.device_features = {};
+        this.flags = {};
+
+        this.on_signal = this.on_signal.bind(this);
+        this.on_data = this.on_data.bind(this);
+
+        this.parse_df_profile(option, 'idf');
+        this.parse_df_profile(option, 'odf');
+    }
+
+    push_data(df_name) {
+        if (this.device_features[df_name].push_data == null)
+            return;
+        let _df_interval = this.interval[df_name] != undefined ? this.interval[df_name] : this.push_interval;
+        console.debug(`${df_name} : ${this.flags[df_name]} [message / ${_df_interval} s]`);
+        let _push_interval = setInterval(() => {
+            let _data = this.device_features[df_name].push_data();
+            if (!this.flags[df_name]) {
+                clearInterval(_push_interval);
+                return;
+            }
+            if (_data === undefined) {
+                return;
+            }
+            this.dan.push(df_name, _data);
+
+        }, _df_interval * 1000);
+    }
+
+    on_signal(signal, df_list) {
+        console.log(`Receive signal: ${signal}, ${df_list}`);
+        if ('CONNECT' == signal) {
+            df_list.forEach(df_name => {
+                if (this.flags[df_name]) {
+                    return;
+                }
+                this.flags[df_name] = true;
+                this.push_data(df_name);
+            });
+        }
+        else if ('DISCONNECT' == signal) {
+            df_list.forEach(df_name => {
+                this.flags[df_name] = false;
+            });
+        }
+        else if ('SUSPEND' == signal) {
+            // Not use
+        }
+        else if ('RESUME' == signal) {
+            // Not use
+        }
+        return true;
+    }
+
+    on_data(df_name, data) {
+        try {
+            this.device_features[df_name].on_data(data);
+        } catch (err) {
+            console.error(err);
+            return false;
+        }
+        return true;
+    }
+
+    df_func_name(df_name) {
+        if (df_name.match(/_[A-Z]?(I|O)[0-9]?$/i)) {
+            return df_name.replace('_', '-');
+        }
+        return df_name;
+    }
+
+    _check_parameter() {
+        if (!this.api_url)
+            throw new RegistrationError('api_url is required.');
+
+        if (!this.device_model)
+            throw new RegistrationError('device_model not given.');
+
+        if (this.persistent_binding && !this.device_addr)
+            throw new ArgumentError('In case of `persistent_binding` set to `True`, ' +
+                'the `device_addr` should be set and fixed.');
+
+        if (Object.keys(this.device_features).length === 0)
+            throw new RegistrationError('Neither idf_list nor odf_list is empty.');
+    }
+
+    run() {
+        this._check_parameter();
+
+        this.dan = new Client();
+
+        let idf_list = [];
+        let odf_list = [];
+
+        for (const [df_name, df] of Object.entries(this.device_features)) {
+            if (df.df_type == 'idf')
+                idf_list.push([df_name, df.df_type]);
+            else
+                odf_list.push([df_name, df.df_type]);
+        }
+
+        const msg = {
+            'url': this.api_url,
+            'on_signal': this.on_signal,
+            'on_data': this.on_data,
+            'accept_protos': ['mqtt'],
+            'id': this.device_addr,
+            'idf_list': idf_list,
+            'odf_list': odf_list,
+            'name': this.device_name,
+            'profile': {
+                'model': this.device_model,
+                'u_name': this.username,
+                'extra_setup_webpage': this.extra_setup_webpage,
+                'device_webpage': this.device_webpage,
+            },
+            'register_callback': this.register_callback,
+            'on_register': this.on_register,
+            'on_deregister': this.on_deregister,
+            'on_connect': this.on_connect,
+            'on_disconnect': () => {
+                for (const key in this.flags) {
+                    this.flags[key] = false;
+                }
+                console.debug(`on_disconnect: _flag = ${this.flags}`);
+                if (on_disconnect) {
+                    return on_disconnect;
+                }
+            }
+        };
+
+        this.dan.register(msg);
+
+        // FIXME: window is not defined in node.js
+        window.onbeforeunload = function () {
+            try {
+                if (!this.persistent_binding) {
+                    this.dan.deregister();
+                }
+            } catch (error) {
+                console.error(`dai process cleanup exception: ${error}`);
+            }
+        };
+    }
+
+    parse_df_profile(option, typ) {
+        const df_list = `${typ}_list`;
+        for (let i = 0; i < option[df_list].length; i++) {
+            let df_name;
+            let param_type;
+            let on_data;
+            let push_data;
+            if (!Array.isArray(option[df_list][i])) {
+                df_name = this.df_func_name(option[df_list][i].name);
+                param_type = null;
+                on_data = push_data = option[df_list][i];
+            }
+            else if (Array.isArray(option[df_list][i]) && option[df_list][i].length == 2) {
+                df_name = this.df_func_name(option[df_list][i][0].name);
+                param_type = option[df_list][i][1];
+                on_data = push_data = option[df_list][i][0];
+            }
+            else {
+                throw new RegistrationError(`Invalid ${df_list}, usage: [df_func, ...] or [[df_func, type], ...]`);
+            }
+
+            let df = new DeviceFeature({
+                'df_name': df_name,
+                'df_type': typ,
+                'param_type': param_type,
+                'push_data': push_data,
+                'on_data': on_data
+            });
+
+            this.device_features[df_name] = df;
+        }
+    }
+}
diff --git a/src/dan.js b/src/dan.js
new file mode 100644
index 0000000..5925126
--- /dev/null
+++ b/src/dan.js
@@ -0,0 +1,335 @@
+import Context from './context.js';
+import _UUID from './uuid.js';
+import mqtt from 'mqtt';
+import superagent from 'superagent';
+import { RegistrationError } from './exceptions.js';
+
+export class Client {
+
+    constructor() {
+        this.ctx = new Context();
+        this._first_publish = false;
+        this._is_reconnect = false;
+        this.on_connect = this.on_connect.bind(this);
+        this.on_disconnect = this.on_disconnect.bind(this);
+    }
+
+    publish(channel, message, retained, qos) {
+        if (!this.ctx.mqtt_client)
+            throw 'unable to publish without ctx.mqtt_client';
+
+        if (retained === undefined)
+            retained = false;
+
+        if (qos === undefined)
+            qos = 2;
+
+        return new Promise((resolve, reject) => {
+            this.ctx.mqtt_client.publish(channel,
+                JSON.stringify(message),
+                { retain: retained, qos: qos, },
+                (err) => {
+                    if (err) {
+                        return reject(err);
+                    }
+                    resolve();
+                }
+            )
+        });
+    }
+
+    subscribe(channel, qos) {
+        if (!this.ctx.mqtt_client)
+            return;
+
+        if (qos === undefined)
+            qos = 2;
+
+        return new Promise((resolve, reject) => {
+            this.ctx.mqtt_client.subscribe(channel,
+                { qos: qos },
+                (err, granted) => {
+                    if (err) {
+                        return reject(err);
+                    }
+                    resolve();
+                });
+        });
+    }
+
+    unsubscribe(channel) {
+        if (!this.ctx.mqtt_client)
+            return;
+
+        return new Promise((resolve, reject) => {
+            this.ctx.mqtt_client.unsubscribe(channel,
+                (err) => {
+                    if (err) {
+                        return reject(err);
+                    }
+                    resolve();
+                });
+        });
+    }
+
+    on_connect() {
+        console.info('mqtt_connect');
+
+        let promise_thing;
+
+        if (!this._is_reconnect) {
+            promise_thing = this.subscribe(this.ctx.o_chans['ctrl'])
+                .then(() => {
+                    console.log(`Successfully connect to ${this.ctx.url}`);
+                    console.log(`Device ID: ${this.ctx.app_id}`);
+                    console.log(`Device name: ${this.ctx.name}.`);
+                    if (typeof (document) !== "undefined") {
+                        document.title = this.ctx.name;
+                    }
+                })
+                .catch(err => {
+                    throw 'Subscribe to control channel failed';
+                });
+        }
+        else {
+            console.info(`Reconnect: ${this.ctx.name}.`);
+            promise_thing = this.publish(
+                this.ctx.i_chans['ctrl'],
+                { 'state': 'offline', 'rev': this.ctx.rev },
+                true // retained message
+            );
+        }
+
+        promise_thing.then(() => {
+            this.ctx.i_chans.remove_all_df();
+            this.ctx.o_chans.remove_all_df();
+
+            this.publish(
+                this.ctx.i_chans['ctrl'],
+                { 'state': 'online', 'rev': this.ctx.rev },
+                true // retained message
+            ).then(() => {
+                this._first_publish = true;
+            });
+
+            this._is_reconnect = true;
+
+            if (this.ctx.on_connect) {
+                this.ctx.on_connect();
+            }
+        }).catch(err => {
+            console.error(err);
+        });
+    }
+
+    on_message(topic, message) {
+        if (topic == this.ctx.o_chans['ctrl']) {
+            const signal = JSON.parse(message);
+            let handling_result = null;
+            switch (signal['command']) {
+                case 'CONNECT':
+                    if ('idf' in signal) {
+                        const idf = signal['idf'];
+                        this.ctx.i_chans.add(idf, signal['topic']);
+                        handling_result = this.ctx.on_signal(signal['command'], [idf]);
+
+                    } else if ('odf' in signal) {
+                        const odf = signal['odf'];
+                        this.ctx.o_chans.add(odf, signal['topic']);
+                        handling_result = this.ctx.on_signal(signal['command'], [odf]);
+                        this.subscribe(this.ctx.o_chans.topic(odf));
+                    }
+                    break;
+                case 'DISCONNECT':
+                    if ('idf' in signal) {
+                        const idf = signal['idf'];
+                        this.ctx.i_chans.remove_df(idf);
+                        handling_result = this.ctx.on_signal(signal['command'], [idf]);
+
+                    } else if ('odf' in signal) {
+                        const odf = signal['odf'];
+                        this.unsubscribe(this.ctx.o_chans.topic(odf));
+                        this.ctx.o_chans.remove_df(odf);
+                        handling_result = this.ctx.on_signal(signal['command'], [odf]);
+                    }
+                    break;
+            }
+            const res_message = {
+                'msg_id': signal['msg_id'],
+            };
+            if (typeof handling_result === 'boolean' && handling_result) {
+                res_message['state'] = 'ok';
+            } else {
+                res_message['state'] = 'error';
+                res_message['reason'] = handling_result[1];
+            }
+            this.publish(this.ctx.i_chans['ctrl'], res_message);
+        }
+        else {
+            let odf = this.ctx.o_chans.df(topic);
+            if (!odf)
+                return;
+            this.ctx.on_data(odf, JSON.parse(message));
+        }
+    }
+
+    on_disconnect() {
+        console.info(`${this.ctx.name} (${this.ctx.app_id}) disconnected from ${this.ctx.url}.`);
+        if (this.ctx.on_disconnect) {
+            this.ctx.on_disconnect();
+        }
+    }
+
+    register(params) {
+        if (this.ctx.mqtt_client) {
+            throw new RegistrationError('Already registered');
+        }
+
+        this.ctx.url = params['url'];
+        if (!this.ctx.url || this.ctx.url == '') {
+            throw new RegistrationError(`Invalid url: ${this.ctx.url}`);
+        }
+
+        this.ctx.app_id = params['id'] || _UUID();
+
+        const body = {
+            'name': params['name'],
+            'idf_list': params['idf_list'],
+            'odf_list': params['odf_list'],
+            'accept_protos': params['accept_protos'] || 'mqtt',
+            'profile': params['profile'],
+        };
+
+        // other callbacks
+        this.ctx.on_register = params['on_register'];
+        this.ctx.on_deregister = params['on_deregister'];
+        this.ctx.on_connect = params['on_connect'];
+        this.ctx.on_disconnect = params['on_disconnect'];
+
+        // filter out the empty `df_list`, in case of empty list, server reponsed 403.
+        ['idf_list', 'odf_list'].forEach(
+            x => {
+                if (Array.isArray(body[x]) && body[x].length == 0)
+                    delete body[x];
+            }
+        );
+
+        superagent.put(`${this.ctx.url}/${this.ctx.app_id}`)
+            .type('json')
+            .accept('json')
+            .send(body)
+            .then(res => {
+                let metadata = res.body;
+                if (typeof metadata === 'string') {
+                    metadata = JSON.parse(metadata);
+                }
+
+                this.ctx.name = metadata['name'];
+                this.ctx.mqtt_host = metadata['url']['host'];
+                this.ctx.mqtt_port = metadata['url']['ws_port'];
+                this.ctx.mqtt_username = metadata['username'] || '';
+                this.ctx.mqtt_password = metadata['password'] || '';
+                this.ctx.i_chans['ctrl'] = metadata['ctrl_chans'][0];
+                this.ctx.o_chans['ctrl'] = metadata['ctrl_chans'][1];
+                this.ctx.rev = metadata['rev'];
+
+                this.ctx.mqtt_client = mqtt.connect(`${metadata.url['ws_scheme']}://${this.ctx.mqtt_host}:${this.ctx.mqtt_port}`, {
+                    clientId: `iottalk-js-${this.ctx.app_id}`,
+                    username: this.ctx.mqtt_username,
+                    password: this.ctx.mqtt_password,
+                    will: {
+                        topic: this.ctx.i_chans['ctrl'],
+                        // in most case of js DA, it never connect back
+                        payload: JSON.stringify({ 'state': 'offline', 'rev': this.ctx.rev }),
+                        retain: true,
+                    },
+                    keepalive: 30,  // seems 60 is problematic for default mosquitto setup
+                });
+
+                this.ctx.mqtt_client.on('connect', this.on_connect);
+                this.ctx.mqtt_client.on('reconnect', () => {
+                    console.info('mqtt_reconnect');
+                });
+                this.ctx.mqtt_client.on('disconnect', this.on_disconnect);
+                this.ctx.mqtt_client.on('error', (error) => {
+                    console.error('mqtt_error', error);
+                });
+                this.ctx.mqtt_client.on('message', (topic, message, packet) => {
+                    this.on_message(topic, message.toString()); // Convert message from Uint8Array to String
+                });
+
+                this.ctx.on_signal = params['on_signal'];
+                this.ctx.on_data = params['on_data'];
+
+                setTimeout(() => {
+                    if (!this._first_publish) {
+                        throw new RegistrationError('MQTT connection timeout');
+                    }
+                }, 5000);
+
+                if (this.ctx.on_register) {
+                    this.ctx.on_register();
+                }
+            })
+            .catch(err => {
+                console.error('on_failure', err);
+            });
+    }
+
+    deregister() {
+        if (!this.ctx.mqtt_client) {
+            throw new RegistrationError('Not registered');
+        }
+
+        this.publish(
+            this.ctx.i_chans['ctrl'],
+            { 'state': 'offline', 'rev': this.ctx.rev },
+            true
+        );
+        this.ctx.mqtt_client.end();
+
+        superagent.del(`${this.ctx.url}/${this.ctx.app_id}`)
+            .type('json')
+            .accept('json')
+            .send(JSON.stringify({ 'rev': this.ctx.rev }))
+            .then(res => {
+                this.ctx.mqtt_client = null;
+                if (this.ctx.on_deregister) {
+                    this.ctx.on_deregister();
+                }
+            }, err => {
+                console.error('deregister fail', err);
+            });
+    }
+
+    push(idf_name, data, qos) {
+        if (!this.ctx.mqtt_client || !this._first_publish) {
+            throw new RegistrationError('Not registered');
+        }
+        if (!this.ctx.i_chans.topic(idf_name)) {
+            return;
+        }
+        if (qos === undefined)
+            qos = 1;
+
+        if (!Array.isArray(data)) {
+            data = [data];
+        }
+
+        this.publish(this.ctx.i_chans.topic(idf_name), data, false, qos);
+    }
+}
+
+let _default_client = new Client();
+
+export function register(...args) {
+    return _default_client.register(...args);
+}
+
+export function deregister() {
+    return _default_client.deregister();
+}
+
+export function push(...args) {
+    return _default_client.push(...args);
+}
diff --git a/src/dan2.js b/src/dan2.js
deleted file mode 100644
index e8b2b68..0000000
--- a/src/dan2.js
+++ /dev/null
@@ -1,244 +0,0 @@
-import ChannelPool from './channel-pool.js'
-import _UUID from './uuid.js'
-import mqtt from 'mqtt'
-import superagent from 'superagent'
-
-let _url;
-let _id;
-let _mqtt_host;
-let _mqtt_port;
-let _mqtt_scheme;
-let _mqtt_client;
-let _i_chans;
-let _o_chans;
-let _ctrl_i;
-let _ctrl_o;
-let _on_signal;
-let _on_data;
-let _rev;
-
-const publish = function(channel, message, retained, qos) {
-  if (!_mqtt_client)
-  {
-    console.warn('unable to publish without _mqtt_client');
-    return;
-  }
-  if (retained === undefined)
-    retained = false;
-  if (qos === undefined)
-    qos = 2;
-
-  _mqtt_client.publish(channel, message, {
-    retain: retained,
-    qos: qos,
-  });
-}
-
-const subscribe = function(channel, qos) {
-  if (!_mqtt_client)
-    return;
-  if (qos === undefined)
-    qos = 2;
-  return _mqtt_client.subscribe(channel, {qos: qos});
-}
-
-const unsubscribe = function(channel) {
-  if (!_mqtt_client)
-    return;
-  return _mqtt_client.unsubscribe(channel);
-}
-
-const on_message = function(topic, message) {
-  if (topic == _ctrl_o) {
-    let signal = JSON.parse(message);
-    let handling_result = null;
-    switch (signal['command']) {
-      case 'CONNECT':
-        if ('idf' in signal) {
-          let idf = signal['idf'];
-          _i_chans.add(idf, signal['topic']);
-          handling_result = _on_signal(signal['command'], [idf]);
-
-        } else if ('odf' in signal) {
-          let odf = signal['odf'];
-          _o_chans.add(odf, signal['topic']);
-          handling_result = _on_signal(signal['command'], [odf]);
-          subscribe(_o_chans.topic(odf));
-        }
-        break;
-      case 'DISCONNECT':
-        if ('idf' in signal) {
-          let idf = signal['idf'];
-          _i_chans.remove_df(idf);
-          handling_result = _on_signal(signal['command'], [idf]);
-
-        } else if ('odf' in signal) {
-          let odf = signal['odf'];
-          unsubscribe(_o_chans.topic(odf));
-          _o_chans.remove_df(odf);
-          handling_result = _on_signal(signal['command'], [odf]);
-        }
-        break;
-    }
-    let res_message = {
-      'msg_id': signal['msg_id'],
-    }
-    if (typeof handling_result == 'boolean' && handling_result) {
-      res_message['state'] = 'ok';
-    } else {
-      res_message['state'] = 'error';
-      res_message['reason'] = handling_result[1];
-    }
-    publish(_ctrl_i, JSON.stringify(res_message));
-    return;
-  }
-  else {
-    let odf = _o_chans.df(topic);
-    if (!odf)
-      return;
-    _on_data(odf, JSON.parse(message));
-  }
-}
-
-export const register = function(url, params, callback) {
-  _url = url;
-  _id = ('id' in params) ? params['id'] : _UUID();
-  _on_signal = params['on_signal'];
-  _on_data = params['on_data'];
-  _i_chans = new ChannelPool();
-  _o_chans = new ChannelPool();
-
-  const on_failure = function(err) {
-    console.error('on_failure', err);
-    if (callback)
-      callback(false, err);
-  };
-
-  let payload = {
-    'name': params['name'],
-    'idf_list': params['idf_list'],
-    'odf_list': params['odf_list'],
-    'accept_protos': params['accept_protos'],
-    'profile': params['profile'],
-  };
-
-  // filter out the empty `df_list`, in case of empty list, server reponsed 403.
-  ['idf_list', 'odf_list'].forEach(
-    x => {
-      if (Array.isArray(payload[x]) && payload[x].length == 0)
-        delete payload[x];
-    }
-  );
-
-  superagent.put(_url + '/' + _id)
-    .type('json')
-    .accept('json')
-    .send(payload)
-    .end((err, res) => {
-      if(err) {
-        on_failure(err);
-        return;
-      }
-
-      let metadata = res.body;
-      console.debug('register metadata', metadata);
-      if (typeof metadata === 'string') {
-        metadata = JSON.parse(metadata);
-      }
-      _rev = metadata['rev'];
-      _ctrl_i = metadata['ctrl_chans'][0];
-      _ctrl_o = metadata['ctrl_chans'][1];
-      _mqtt_host = metadata.url['host'];
-      _mqtt_port = metadata.url['ws_port'];
-      _mqtt_scheme = metadata.url['ws_scheme'];
-
-      function on_connect() {
-        console.info('mqtt_connect');
-        _i_chans.remove_all_df();
-        _o_chans.remove_all_df();
-        publish(
-          _ctrl_i,
-          JSON.stringify({'state': 'online', 'rev': _rev}),
-          true // retained message
-        );
-        subscribe(_ctrl_o);
-        if (callback) {
-          callback({
-            'raproto': _url,
-            'mqtt': metadata['url'],
-            'id': _id,
-            'd_name': metadata['name'],
-          });
-        }
-      }
-
-      _mqtt_client = mqtt.connect(_mqtt_scheme + '://' + _mqtt_host + ':' + _mqtt_port, {
-        clientId: 'mqttjs_' + _id,
-        will: {
-          topic: _ctrl_i,
-          // in most case of js DA, it never connect back
-          payload: JSON.stringify({'state': 'offline', 'rev': _rev}),
-          retain: true,
-        },
-        keepalive: 30,  // seems 60 is problematic for default mosquitto setup 
-      });
-      _mqtt_client.on('connect', on_connect);
-      _mqtt_client.on('reconnect', () => { console.info('mqtt_reconnect'); });
-      _mqtt_client.on('error', (err) => { console.error('mqtt_error', err); });
-      _mqtt_client.on('message', (topic, message, packet) => {
-        // Convert message from Uint8Array to String
-        on_message(topic, message.toString());
-      });
-
-    });
-}
-
-export const deregister = function(callback) {
-  if (!_mqtt_client) {
-    if (callback)
-      return callback(true);
-    return;
-  }
-
-  publish(
-    _ctrl_i,
-    JSON.stringify({'state': 'offline', 'rev': _rev})
-  );
-  _mqtt_client.end();
-  superagent.del(_url +'/'+ _id)
-    .set('Content-Type', 'application/json')
-    .set('Accept', '*/*')
-    .send(JSON.stringify({'rev': _rev}))
-    .end((err, res) => {
-      if(err) {
-        console.error('deregister fail', err);
-        if (callback)
-          return callback(false, err);
-      }
-    });
-
-  if (callback)
-    return callback(true);
-}
-
-export const push = function(idf_name, data, qos) {
-  if (!_mqtt_client || !_i_chans.topic(idf_name))
-    return;
-  if(qos === undefined)
-    qos = 1;
-  publish(_i_chans.topic(idf_name), JSON.stringify(data), false, qos);
-}
-
-export const UUID = function() {
-  return _id ? _id : _UUID();
-}
-
-export const connected = function() {
-  if( typeof _mqtt_client !== 'object' ) return false;
-  return _mqtt_client.connected;
-}
-
-export const reconnecting = function() {
-  if( typeof _mqtt_client !== 'object' ) return false;
-  return _mqtt_client.reconnecting;
-}
diff --git a/src/device-feature.js b/src/device-feature.js
new file mode 100644
index 0000000..48314c5
--- /dev/null
+++ b/src/device-feature.js
@@ -0,0 +1,25 @@
+import { ArgumentError } from './exceptions.js';
+export default class {
+
+    constructor(params) {
+        this.df_name = params['df_name'];
+        if (!this.df_name) {
+            throw new ArgumentError('device feature name is required.');
+        }
+
+        this.df_type = params['df_type'];  // idf | odf
+        if (this.df_type != 'idf' && this.df_type != 'odf') {
+            throw new ArgumentError(`${this.df_name} df_type must be "idf" or "odf"`);
+        }
+
+        this.param_type = params['param_type'] || [null];
+
+        this.on_data = null
+        if (params['df_type'] == 'odf' && params['on_data'])
+            this.on_data = params['on_data'];
+
+        this.push_data = null
+        if (params['df_type'] == 'idf' && params['push_data'])
+            this.push_data = params['push_data'];
+    }
+}
diff --git a/src/exceptions.js b/src/exceptions.js
new file mode 100644
index 0000000..680e312
--- /dev/null
+++ b/src/exceptions.js
@@ -0,0 +1,10 @@
+class CustomError extends Error {
+    constructor(message) {
+        super(message);
+        this.name = this.constructor.name;
+    }
+}
+
+export class ArgumentError extends CustomError { }
+
+export class RegistrationError extends CustomError { }
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..c7dd058
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,3 @@
+import dai from './dai.js';
+import * as dan from './dan.js';
+export { dai, dan };
diff --git a/webpack.config.js b/webpack.config.js
index 92cdd01..f31ce56 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,23 +1,26 @@
 const webpack = require('webpack');
 
-module.exports = {
-  target: 'web',
-  entry: __dirname + '/src/dan2.js',
-  output: {
-    path: __dirname + '/build-web',
-    filename: 'dan2-web.js',
-    library: ['dan2'],
-    libraryTarget: 'window',
-  },
-  module: {
-    rules: [
-      {
-        test: /\.m?js$/,
-        exclude: /node_modules/,
-        use: {
-          loader: 'babel-loader',
-        }
-      },
-    ]
+module.exports = [
+  {
+    mode: 'none',
+    target: 'web',
+    entry: __dirname + '/src/index.js',
+    output: {
+      path: __dirname + '/build-web',
+      filename: 'iottalkjs-web.js',
+      library: 'iottalkjs',
+      libraryTarget: 'window',
+    },
+    module: {
+      rules: [
+        {
+          test: /\.m?js$/,
+          exclude: /node_modules/,
+          use: {
+            loader: 'babel-loader',
+          }
+        },
+      ]
+    }
   }
-}
+]