Skip to content
uupaa edited this page Nov 17, 2014 · 80 revisions

WebWorker.js は、WebWorkers の機能を薄くラップしたものです。

Main Thread 側で機能する lib/WebWorker.js と、Worker 側で機能する lib/WorkerThread.js で構成されています。

Main Thread                             |     Worker 
----------------------------------------+----------------------------------------
worker = new WebWorker(source,callback) |
                                        |     worker = new WorkerThread(callback)
                                        |
worker.request(requestMessage)       ---|--> callback(requestMessage)
                                        |
callback(err,responseMessage)        <--|--- worker.respnse(responseMessage)

  • WebWorker から WorkerThread 側に送るメッセージをリクエストと呼びます
  • WorkerThread から WebWorker 側に返すメッセージをレスポンスと呼びます
  • リクエストは WebWorker#request で生成します。
    生成したリクエストは new WorkerThread(callback) の callback に届きます
  • レスポンスは WorkerThread#respnse で生成します。
    生成したレスポンスは new WebWorker(, callback) の callback に届きます
  • リクエストを追跡するための情報を Ticket と呼びます。
    チケットは、リクエスト時に発行され、レスポンスの完了で消費され、使用済みになります。
  • リクエストとレスポンスの状況を確認するには、
    • new WebWorker(,, { verbose: true }) または DevTools で WebWorker.dump() を実行します

[WebWorkers の勘所](WebWorkers Crux)も参考にしてください。

ユースケース

WebWorker と WorkerThread の基本的なやりとり

以下は、WebWorker からのリクエストと WorkerThread からのレスポンスの基本的な流れです。

Main Thread Worker
worker = new WebWorker(source, callback) worker = new WorkerThread(callback)
1 worker.request(body)
2 callback(body) { ... }が呼ばれる
3 worker.response(body) でレスポンスを返す
4 callback(err, body) { ... } が呼ばれる

on(method) による呼び出し

WebWorker#on(method, methodCallback) を実行しておくと、method に対するレスポンスを methodCallback で受け取る事ができます。
同様に、WorkerThread#on(method, methodCallback) を実行しておくことで、method に対するリクエストを methodCallback で受け取る事ができます。

Main Thread WorkerGlobalScope
worker = new WebWorker(source) worker = new WorkerThread)
on worker.on("Foo", methodCallback) worker.on("Bar", methodCallback)
1 worker.request(body, { method: "Bar" })
2 methodCallback(body) { ... }が呼ばれる
3 worker.response(body, { method: "Bar" }) でレスポンスを返す
4 methodCallback(err, body) { ... } が呼ばれる

WebWorker API

WebWorker

new WebWorker(source:URLString|JavaScriptFragmentString, callback:Function = null, options:Object = {}) は、WebWorker のインスタンスを生成し返します。

source には Worker を生成するためのソースコードを指定します。 callback は WorkerThread#response のタイミングでコールバックされます。

callback は callback(err:Error|null, body:Any, param:Object):void の形でコールバックされます。
body には WorkerThread#response(body:Any, options:Object) で渡された body がそのまま渡されます。
Worker でエラーが発生した場合は、err に ErrorObject が格納されています。

options には以下の値を指定できます。

property:type = default value
name:String = "" WebWorker のインスタンスに設定する名前を指定します。省略した場合は1から始まる数字になります
origin:String = "" WorkerGlobalScopre の self.origin の値を指定します
inline:Boolean = false source に inline Worker 用の文字列を指定した場合に true にします
import:URLStringArray = [] WorkerThread の初期化時に、一緒に読み込むスクリプトのURLを配列で指定します
verbose:Boolean = false verbose mode にします。
リクエストとレスポンスに関するログを出力します
function WebWorker(source,    // @arg URLString|JavaScriptFragmentString - new Worker(source) or inline worker source
                   callback,  // @arg Function = null - callback(err:Error, body:Any, param:Object):void
                   options) { // @arg Object - { name, origin, import, inline, verbose }
                              // @options.name String = "" - Worker name.
                              // @options.origin URLString = "" - set WorkerGlobalScope.origin.
                              // @options.import URLStringArray = [] - importScripts(...) files.
                              // @options.inline Boolean = false - use inline WebWorker.
                              // @options.verbose Boolean = false - verbose mode.
                              // @desc init Worker session.
//{@dev
    $valid($type(source,         "String"),        WebWorker, "source");
    $valid($type(callback,       "Function|omit"), WebWorker, "callback");
    $valid($type(options,        "Object|null"),   WebWorker, "options");
    $valid($keys(options,        "name|origin|import|inline|verbose"),
                                                   WebWorker, "options");
    $valid($type(options.name,   "String|omit"),   WebWorker, "options.name");
    $valid($type(options.origin, "String|omit"),   WebWorker, "options.origin");
    $valid($type(options.import, "Array|omit"),    WebWorker, "options.import");
    $valid($type(options.inline, "Boolean|omit"),  WebWorker, "options.inline");
    $valid($type(options.verbose,"Boolean|omit"),  WebWorker, "options.verbose");
//}@dev

    this._source    = source;
    this._callback  = callback || null;
    this._name      = options["name"]    || "";
    this._origin    = options["origin"]  || "";
    this._import    = options["import"]  || [];
    this._inline    = options["inline"]  || "";
    this._verbose   = options["verbose"] || false;

//{@dev
    $valid(WMURL.isValid(this._origin), WebWorker, "options.origin");
    $valid(WMURL.isValid(this._import), WebWorker, "options.import");
//}@dev

    this._on        = {}; // { method: callback, ... }
    this._blobURL   = ""; // blob url for inline worker
    this._worker    = null; // new Worker(source) instance
    this._workerID  = ++_workerID;
    this._requestID = 0;
    this._name      = this._name || this._workerID;

    _tickets[this._name] = _tickets[this._name] || {};
}
    var scripts = [
                "../node_modules/uupaa.valid.js/lib/Valid.js",
                "../node_modules/uupaa.task.js/lib/Task.js"
            ];

    var worker = new WebWorker("./worker.import.js", function(err, body, param) {
            if (err) {
                ;
            } else {
                if (body.result === "OK") {

                    //task.pass();
                    return;
                }
            }
            //task.miss();
        }, { "import": scripts, verbose: true });

    worker.request();
importScripts("../lib/WorkerThread.js");

var worker = new WorkerThread(function(body, param) {

    new Task(1, function(err) {

        worker.response({ "result": "OK" });

    }).pass();
});

WebWorker.prototype.request

WebWorker#request(body:Any, options:Object = {}):TicketString は、 必要に応じて Worker を生成し、 WebWorker ⇔ WorkerThread 間でリクエストを追跡するための各種情報と共に、Worker にメッセージを飛ばします、 リクエストを追跡するためのユニークな文字列(TicketString)を返します。

options

options には以下のキーワードを指定できます

key:type = default value
options.transfer:Array = null transferable object を指定します
options.ticket:String = "" 外部で生成した ticket を指定する場合に使用します。通常は使用しません
options.method:String = "" WorkerThread#on(method, callback) を呼び出す場合に指定します
options.reply:String = "" WorkerThread#response からのレスポンスを受けとるメソッドを強制します。通常は使用しません

リクエストの内容

WebWorker#request が生成するリクエスト(requestStructure)の詳細です。

key:type = default value
requestStructure.init:Boolean = false Worker の初期化が必要なタイミング(初回リクエスト)で true になります
requestStructure.origin:String = "" options.origin の値です
requestStructure.import:URLStringArray = [] options.import の値です
requestStructure.ticket:String リクエストを追跡するためのIDです
requestStructure.method:String = "" options.method の値です
requestStructure.reply:String = "" options.reply の値です
requestStructure.cancel:Boolean = false WebWorker#cancel を実行した場合に true になります
requestStructure.body:Any body の値です
function WebWorker_request(body,      // @arg Any - request body.
                           options) { // @arg Object = {} - { transfer, ticket, method, reply }
                                      // @options.transfer Array = null - transferable object
                                      // @options.ticket TicketString = ""
                                      // @options.method MethodNameString = ""
                                      // @options.reply MethodNameString = ""
                                      // @ret TicketString
                                      // @desc open and request Worker session.
//{@dev
    $valid($type(options, "Object|omit"), WebWorker_request, "options");
    $valid($keys(options, "transfer|ticket|method|reply"), WebWorker_request, "options");
//}@dev

    options = options || {};

    var transfer = options["transfer"] || null;
    var ticket   = options["ticket"]   || _makeTicket(this._workerID, ++this._requestID); // {{WORKER_ID}}.{{REQUEST_ID}}
    var method   = options["method"]   || "";
    var reply    = options["reply"]    || "";

//{@dev
    $valid($type(options.transfer, "Array|omit"),  WebWorker_request, "options.transfer");
    $valid($type(options.ticket,   "String|omit"), WebWorker_request, "options.ticket");
    $valid($type(options.method,   "String|omit"), WebWorker_request, "options.method");
    $valid($type(options.reply,    "String|omit"), WebWorker_request, "options.reply");
//}@dev

    var initialized = !!this._worker;
    var requestStructure = {
            "init":     initialized ? 0  : 1, // false/true -> 0/1
            "origin":   initialized ? "" : this._origin,
            "import":   initialized ? "" : this._import.join(","), // ArrayString -> CommaJointString
          //"error":    "",
            "ticket":   ticket,
            "method":   method,
            "reply":    reply,
            "cancel":   0,
            "body":     body
        };

    if (!initialized) {
        _initWorker(this);
    }

    _updateTicketState(this._name, ticket, _STATE_REQUEST, method);

//{@verbose
    if (this._verbose && global["console"]) {
        var logMessage = "--\x3e WebWorker(" + this._name + ")#request(" + ticket + ", " + method + ", " + reply + ")";

        if (console.group) {
            console.group(logMessage);
        } else {
            console.log(logMessage);
        }
    }
//}@verbose

    // send request to worker
    if (transfer) {
        this._worker["postMessage"](requestStructure, transfer);
    } else {
        this._worker["postMessage"](requestStructure);
    }
    return ticket;
}

transferableObjects によるゼロコピー

WebWorker#request(, options) の options.transfer に transferable object を指定すると、Worker 側に ArrayBuffer の参照を渡す事ができます。
詳しくは、 Transferable Objects を参照してください。
transferableObjects に指定したオブジェクトは所有権が Worker 側に移り、以後 UI Thread からはアクセスできなくなります(配列を渡した場合は、配列のlengthが 0 になります)。

// Transferable Objects を指定した WebWorker#request
var worker = new WebWorker("./worker2.js", function(err, body, param) {
});

var bigTypedArray = new Uint8Array(1024 * 1024 * 4); // 4MB

worker.request(bigTypedArray.buffer, { transfer: [ bigTypedArray.buffer ] });
importScripts("../lib/WorkerThread.js");

var worker = new WorkerThread(function(body, param) {
    // body は bigTypedArray.buffer です。4MB のデータが格納されています。

    // 以下のようにすると、 Main Thread 側に送り返すも事ができます。
    worker.response(body, { transfer: [ body ] });
});

WebWorker.prototype.on

WebWorker#on(method:String, methodCallback:Function):this は、method に対応する methodCallback を登録します。

WebWorker#on("Foo", methodCallback); の状態で、 WorkerThread#response(body, { method: "Foo" }) を実行すると、methodCallback が呼ばれます。

コールバックの引数は methodCallback(err:Error|null, body:Any, param:Object):void になります。

method は複数登録できません。複数登録すると、最後に登録した methodCallback が呼ばれます。

function WebWorker_on(method,           // @arg MethodNameString
                      methodCallback) { // @arg Function - methodCallback(err:Error, body:Any, param:Object):void
                                        // @ret this
//{@dev
    $valid($type(method,         "MethodNameString"), WebWorker_on, "method");
    $valid($type(methodCallback, "Function"),         WebWorker_on, "methodCallback");
//}@dev

    this._on[method] = methodCallback;
    return this;
}

WebWorker.prototype.off

WebWorker#off(method):this は、WebWorker#on を取り消します。

function WebWorker_off(method) { // @arg MethodNameString
                                 // @ret this
//{@dev
    $valid($type(method, "MethodNameString"), WebWorker_off, "method");
//}@dev

    delete this._on[method];
    return this;
}

WebWorker.prototype.cancel

WebWorker#cancel(ticket:String):Boolean は、WorkerThread#on("cancel", callback) を呼び出します。

ticket には WebWorker#request が返す ticket を指定します。このメソッドは、既に発行済みのリクエストをキャンセルするために使用します。

function WebWorker_cancel(ticket) { // @arg TicketString
                                    // @ret Boolean
//{@dev
    $valid($type(ticket, "String"), WebWorker_cancel, "ticket");
//}@dev

    if (_tickets[this._name][ticket]["state"] === _STATE_REQUEST) {
        _updateTicketState(this._name, ticket, _STATE_CANCEL, "cancel");

//{@verbose
        if (this._verbose && global["console"]) {
            console.log("--\x3e WebWorker(" + this._name + ")#cancel(" + ticket + ")");
        }
//}@verbose

        var requestStructure = { "ticket": ticket, "cancel": 1 };

        this._worker["postMessage"](requestStructure);
        return true;
    }
    return false;
}

WebWorker.prototype.close

WebWorker#close() は、Worker を終了させ、インスタンスの終了処理を行います。
WebWorker#close() 実行後に、WebWorker#request で再度リクエストを行うことも可能です。
close 後に、request を実行した場合は再度初期化が行われ、新しい Worker が生成されます。

function WebWorker_close() { // @desc close all Workers session
    var that = this;

    that._worker.removeEventListener("message", that);
    that._worker.removeEventListener("error",   that);
    that._worker["terminate"]();
    that._worker = null; // [!][GC]

    _updateTicketState(that._name, 0, _STATE_CLOSED, "");

    if (that._inline) {
        _URL["revokeObjectURL"](that._blobURL); // [!] GC
        that._blobURL = "";
    }
}

WebWorker.prototype.inbox

WebWorker#inbox(task:Task, body:Any, id:String):void は、Message.js と連携して機能し、WorkerThread#inbox メソッドを呼び出します。

function WebWorker_inbox(task, // @arg Task
                         body, // @arg Any
                         id) { // @arg String - instance id
                               // @desc Message.js over WebWorker.js
//{@dev
    $valid($type(task, "Task"),   WebWorker_inbox, "task");
    $valid($type(id,   "String"), WebWorker_inbox, "id");
//}@dev

    var that   = this;
    var ticket = _makeTicket(that._workerID, ++that._requestID);
    var unique = "__REPLY__" + ticket; // make unique string

    that["on"](unique, function(err, body) {
        that["off"](unique);
        if (!err) {
            task["set"](id, body);
        }
        task["done"](err);
    });
    return that["request"](body, { "ticket": ticket,
                                   "method": "inbox", "reply": unique });
}

WorkerThread API

WorkerThread

new WorkerThread(callback:Function) は、WorkerThread のインスタンスを生成します。

callback は WebWorker#request のタイミングでコールバックされます。

callback は callback(body:Any, param:Object):void の形でコールバックされます。
body には WebWorker#request(body:Any, options:Object) で渡された body がそのまま渡されます。

WorkerThread.prototype.on

WorkerThread#on(method:String, methodCallback:Function):this は、method に対応する methodCallback を登録します。

WorkerThread#on("Bar", methodCallback); の状態で、 WebWorker#request(body, { method: "Bar" }) を実行すると、methodCallback が呼ばれます。

コールバックの引数は methodCallback(body:Any, param:Object):void になります。

method は複数登録できません。複数登録すると、最後に登録した methodCallback が呼ばれます。

function WorkerThread_on(method,           // @arg MethodNameString
                         methodCallback) { // @arg Function - methodCallback(body:Any, param:Object):void
                                           // @ret this
    this._on[method] = methodCallback;
    return this;
}

WorkerThread.prototype.off

WorkerThread#off(method):this は、WorkerThread#on を取り消します。

function WorkerThread_off(method) { // @arg MethodNameString
                                    // @ret this
    delete this._on[method];
    return this;
}

WorkerThread.prototype.response

WorkerThread#response(body:Any, options:Object = {}):TicketString は、レスポンスを送信します。

WorkerThread#response が生成するリクエスト(responseStructure)の詳細です。

key:type = default value
responseStructure.error:String = "" エラー文字列です
responseStructure.ticket:String リクエストを追跡するためのIDです
responseStructure.method:String = "" options.method の値です
responseStructure.body:Any body の値です
function WorkerThread_response(body,      // @arg Any - response data
                               options) { // @arg Object = {} - { transfer, ticket, method, error, reply }
                                          // @options.transfer Array = null - Transferable Objects.
                                          // @options.ticket TicketString = ""
                                          // @options.method MethodNameString = ""
                                          // @options.error ErrorObject = null
                                          // @options.reply MethodNameString = ""
                                          // @ret TicketString
    options = options || {};

    var transfer = options["transfer"] || null;
    var ticket   = options["ticket"]   || this._ticket;
    var method   = options["reply"]    || // [!] The reply take precedence over method.
                   options["method"]   || "";
    var error    = options["error"]    || null;

    var responseStructure = {
          //"init":     0,
          //"origin":   "",
          //"import":   "",
            "error":    error ? error.message : "",
            "ticket":   ticket,
            "method":   method,
          //"reply":    "",
          //"cancel":   0,
            "body":     body
        };

    if (transfer && !error) {
        global["postMessage"](responseStructure, transfer);
    } else {
        global["postMessage"](responseStructure);
    }
    return ticket;
}