Storage


    var local_storage = document.getElementById("local_storage");
    var session_storage = document.getElementById("session_storage");
    var session_storage = document.getElementById("session_storage");

    function storageClickCounter() {
      if(typeof(Storage) !== "undefined") {
        if (localStorage.clickcount) {
          localStorage.clickcount = Number(localStorage.clickcount)+1;
        } else {
          localStorage.clickcount = 1;
        }
        if (sessionStorage.clickcount) {
          sessionStorage.clickcount = Number(sessionStorage.clickcount)+1;
        } else {
          sessionStorage.clickcount = 1;
        }
        local_storage.innerHTML = "localStorage: " + localStorage.clickcount;
        session_storage.innerHTML = "sessionStorage: " + sessionStorage.clickcount;
      } else {
        local_storage.innerHTML = "Sorry, your browser does not support web storage...";
      }
    }

    function storageClickCounterReset() {
      if(typeof(Storage) !== "undefined") {
        localStorage.clickcount = 0;
        sessionStorage.clickcount = 0;
        local_storage.innerHTML = "localStorage have no clicks registered";
        session_storage.innerHTML = "sessionStorage have no clicks registered";
      } else {
        local_storage.innerHTML = "Sorry, your browser does not support web storage...";
      }
    }

    ;(function () {
      if(typeof(Storage) !== "undefined") {
        if (localStorage.clickcount) {
          local_storage.innerHTML = "localStorage: " + localStorage.clickcount;
        } else {
          local_storage.innerHTML = "localStorage have no clicks registered";
        }
        if (sessionStorage.clickcount) {
          session_storage.innerHTML = "sessionStorage: " + sessionStorage.clickcount;
        } else {
          session_storage.innerHTML = "sessionStorage have no clicks registered";
        }
      } else {
        local_storage.innerHTML = "Sorry, your browser does not support web storage...";
      }
    })();
  

IndexedDB

db_name:

db_store:

obj_key:


    var idb_result = document.getElementById("idb_result"),
        db,
        db_name = document.getElementById("db_name"),
        db_store = document.getElementById("db_store"),
        obj_key = document.getElementById("obj_key");
    var rnd_str = () => {
      var text = "";
      var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
      for (var i = 0; i < 3; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
      }
      return text;
    };
    var rnd_int = () => (Math.floor(Math.random() * 1000) + 1);

    function initDB() {
      idb_result.innerHTML = '';
      window.indexedDB =  window.indexedDB ||
                          window.mozIndexedDB ||
                          window.webkitIndexedDB ||
                          window.msIndexedDB;
      // you may need references to some window.IDB* objects:
      window.IDBTransaction = window.IDBTransaction ||
                              window.webkitIDBTransaction ||
                              window.msIDBTransaction;
      window.IDBKeyRange =  window.IDBKeyRange ||
                            window.webkitIDBKeyRange ||
                            window.msIDBKeyRange;
      if(!window.indexedDB) {
        idb_result.innerHTML += 'Unable to init IndexDB objects !!!';
      }

      // open database
      var DBOpenRequest = window.indexedDB.open(db_name.value); // .open(db_name, 3)
      DBOpenRequest.onblocked = function() {
        idb_result.innerHTML += 'Database is open somewhere else (onblocked)';
      };
      DBOpenRequest.onerror = function(event) {
        idb_result.innerHTML += 'Error loading database (onerror)';
      };
      DBOpenRequest.onsuccess = (event) => {
        db = DBOpenRequest.result; // save connection
        db.onerror = function(event) {
          idb_result.innerHTML += 'Database error: '+event.target.errorCode+'';
        };
        idb_result.innerHTML += 'Database ['+db.name+'] [v.'+db.version+'] initialised, stores: ';
        for (store of db.objectStoreNames) {
          idb_result.innerHTML += '['+store+'] ';
        }
        idb_result.innerHTML += '';
      };

      // create or update db
      DBOpenRequest.onupgradeneeded = (event) => {
        var db = DBOpenRequest.result;
        db.onerror = function(event) {
          idb_result.innerHTML += 'Error loading database (onerror)';
        };
        db.onabort = function() {
          idb_result.innerHTML += 'Error loading database (onabort)';
        };
        db.onversionchange = function(event) {
          idb_result.innerHTML += 'Database change has occurred (onversionchange)';

          if (event.newVersion == 2) {
            db.deleteObjectStore(db_store.value); // only inside upgrade and versionchange event
          }

        };
        db.onclose = function(event) {
          idb_result.innerHTML += 'Database [' + db.name + '] has (unexpectedly?) closed (onclose)';
        };

        // database doesnt exist yet, the value of oldVersion is 0
        if (event.oldVersion < 1) {
          // Create an objectStore for this database
          var objectStore = db.createObjectStore(db_store.value, { keyPath: "title" });
          // define what data items the objectStore will contain
          objectStore.createIndex("title", "title", { unique: false });
          objectStore.createIndex("listeners", "listeners", { unique: false });

          var initial_albums = [
            {
              title:"any_title",
              listeners:5345
            }
          ];
          initial_albums.forEach(function(customer) {
            objectStore.add(customer);
          });
        }

        // // MIGRATIONS, consecutive schema update
        // if (event.oldVersion < 2) {
        //   // version 2 introduces a new index of books by year.
        //   var bookStore = DBOpenRequest.transaction.objectStore("books");
        //   var yearIndex = bookStore.createIndex("by_year", "year");
        //   var del_index = objectStore.deleteIndex("contact");
        //   // ... version 3 introduces a new object store for magazines with two indexes.
        //   var magazines = db.createObjectStore("magazines");
        //   var publisherIndex = magazines.createIndex("by_publisher", "publisher");
        //   var frequencyIndex = magazines.createIndex("by_frequency", "frequency");
        //   // ... version 4
        //   db.createObjectStore("names", { autoIncrement : true });
        //   // ... version 5
        //   db.deleteObjectStore("names"); // only inside upgrade and versionchange event
        // }

      };
    }
    function get_transaction (t_mode = "readwrite") {
      if(!db) return;
      // transaction = db.transaction(db.objectStoreNames); // all stores
      // specific store
      // opens an object store that we can then manipulate the data inside of
      // var objectStore = db.transaction('toDoList').objectStore('toDoList');
      var transaction = db.transaction([db_store.value], t_mode);
      // report on the success of opening the transaction
      transaction.oncomplete = function(event) {
        idb_result.innerHTML += 'Transaction completed';
      };
      transaction.onerror = function(event) {
        idb_result.innerHTML += 'Transaction not opened due to error: '+transaction.error+'';
      };
      return transaction;
    }
    function closeDB() {
      if(!db) return;
      idb_result.innerHTML = '';
      var db_close = db.close();
      db=undefined;
      idb_result.innerHTML += '['+db_name.value+'] database closed';
    }
    function clearStore() {
      if(!db) return;
      idb_result.innerHTML = '';
      var store_clear = get_transaction().objectStore(db_store.value).clear();
      store_clear.onsuccess = function(event) {
        idb_result.innerHTML += '['+db_store.value+'] store cleared successfully';
      };
      store_clear.onsuccess = function(event) {
        idb_result.innerHTML += 'Error clearing ['+db_store.value+'] store';
      };
    }
    function deleteDB() {
      idb_result.innerHTML = '';
      closeDB();
      var db_del = window.indexedDB.deleteDatabase(db_name.value);
      db_del.onerror = function(event) {
        idb_result.innerHTML += 'Error deleting ['+db_name.value+'] database';
      };
      db_del.onsuccess = function(event) {
        idb_result.innerHTML += 'Database ['+db_name.value+'] deleted successfully';
      };
    }
    function deleteStore() {
      idb_result.innerHTML = '';
      // call higher version to delete inside version change event
      window.indexedDB.open(db_name.value, 2);
    }
    function addData() {
      if(!db) return;
      idb_result.innerHTML = '';
      var objectStore = get_transaction().objectStore(db_store.value);
      var data_obj = {
        title:rnd_str(),
        listeners:rnd_int()
        // ,blob: xhr.response
      };
      var objectStore_request = objectStore.add(data_obj);
      objectStore_request.onsuccess = function(event) {
        // report the success of the request (this does not mean the item
        // has been stored successfully in the DB - for that you need transaction.onsuccess)
        idb_result.innerHTML += 'Data added: ';
        for (x in data_obj) {
          idb_result.innerHTML += x+': '+data_obj[x]+' ';
        }
        idb_result.innerHTML += '';
      };
    }
    function getByKey() {
      if(!db) return;
      idb_result.innerHTML = '';
      get_transaction()
      .objectStore(db_store.value)
      .get(obj_key.value)
      .onsuccess = function(event) {
        if(!event.target.result) {
          idb_result.innerHTML += 'Nothing found !';
          return;
        }
        idb_result.innerHTML += 'Data by key: ';
        for (x in event.target.result) {
          idb_result.innerHTML += x+': '+event.target.result[x]+' ';
        }
        idb_result.innerHTML += '';
      };
    }
    function deleteByKey() {
      if(!db) return;
      idb_result.innerHTML = '';
      get_transaction()
      .objectStore(db_store.value)
      .delete(obj_key.value)
      .onsuccess = function(event) {
        idb_result.innerHTML += 'Item with key '+obj_key.value+' deleted';
      };
    }
    function updateByKey() {
      if(!db) return;
      idb_result.innerHTML = '';
      var objectStore = get_transaction().objectStore(db_store.value);
      objectStore
      .get(obj_key.value)
      .onsuccess = function(event) {
        var data = event.target.result;
        data.listeners = data.listeners + 1000
        objectStore.put(data).onsuccess = function() {
          idb_result.innerHTML += 'Added 1000 listeners to '+data.title+'';
        };
      };
    }
    function countItems() {
      if(!db) return;
      idb_result.innerHTML = '';
      get_transaction()
      .objectStore(db_store.value)
      .count()
      .onsuccess = function(event) {
        idb_result.innerHTML += event.target.result+' items';
      }
    }
    function dataViaCursor(in_range = false) {
      if(!db) return;
      idb_result.innerHTML = '';
      if(in_range) {
        var keyRangeValue = IDBKeyRange.bound("A", "T", true, true);
        console.log([
          keyRangeValue.includes('W'), // if in range
          keyRangeValue.lower,
          keyRangeValue.upper,
          keyRangeValue.lowerOpen,
          keyRangeValue.upperOpen
        ]);
      }

      // // only "Donna"
      // IDBKeyRange.only("Donna");
      // // anything past "Bill", including "Bill"
      // IDBKeyRange.lowerBound("Bill");
      // // anything past "Bill", but don't include "Bill"
      // IDBKeyRange.lowerBound("Bill", true);
      // // anything up to, but not including, "Donna"
      // IDBKeyRange.upperBound("Donna", true);
      // // anything between "Bill" and "Donna", but not including "Donna"
      // IDBKeyRange.bound("Bill", "Donna", false, true);

      var objectStore = get_transaction().objectStore(db_store.value);

      if(in_range) {
        // in range in reverse order
        var osc = objectStore.openCursor(keyRangeValue,'prev');
      }
      else {
        // objectStore.openCursor(null,'prev');
        // nextunique, prevunique
        var osc = objectStore.openCursor();
      }

      osc.onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {

          for (x in cursor.value) {
            idb_result.innerHTML += x+': '+cursor.value[x]+' ';
          }
          idb_result.innerHTML += '';

          // // Abort the transaction we just did
          // objectStore.transaction.abort();return;

          console.log([
            cursor.key,
            cursor.value,
            cursor.primaryKey,
            cursor.source,
            cursor.direction
          ]);

          cursor.continue();
          // cursor.advance(2);
        } else {
          idb_result.innerHTML += 'All entries displayed';
        }
      };

      console.log([
        // transaction.db,
        // transaction.mode,
        // transaction.objectStoreNames,
        objectStore.indexNames,
        objectStore.keyPath,
        objectStore.name, // available to rename
        objectStore.transaction,
        objectStore.autoIncrement
      ]);

      // var request = objectStore.openKeyCursor();
      // request.onsuccess = function(event) {
      //   var cursor = event.target.result;
      //   if(cursor) {
      //     // cursor.key contains the key of the current record being iterated through
      //     // note that there is no cursor.value, unlike for openCursor
      //     // this is where you'd do something with the result
      //     cursor.continue();
      //   } else {
      //     // no more results
      //   }
      // };

      // let today = new Date();
      // let yesterday = new Date(today);
      // yesterday.setDate(today.getDate() - 1);
      // let request = store.getKey(IDBKeyRange(yesterday, today));
      // request.onsuccess = (event) => {
      //   let when = event.target.result;
      //   alert("The 1st activity in last 24 hours was occurred at " + when);
      // };
    }
    function dataViaIndex() {
      if(!db) return;
      idb_result.innerHTML = '';
      var transaction = get_transaction('readonly');
      var objectStore = transaction.objectStore(db_store.value);
      var myIndex = objectStore.index('title');

      console.log([
        myIndex.isAutoLocale,
        myIndex.locale,
        myIndex.name,
        myIndex.objectStore,
        myIndex.keyPath,
        myIndex.multiEntry,
        myIndex.unique
      ]);

      myIndex.count().onsuccess = function(event) {
        idb_result.innerHTML += event.target.result+' items';
      }
      myIndex.getAllKeys().onsuccess = function(event) {
        idb_result.innerHTML += 'all keys: '+event.target.result+'';
      }
      // myIndex.myIndex.get('Bungle')
      // myIndex.getKey('Bungle')
      // myIndex.getAll()

      // let count = 0;
      // let unreadList = [];
      myIndex.openCursor().onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {

          // modify item while iterating
          if (cursor.value.title === 'AAA') {
            // // update
            // const updateData = cursor.value;
            // updateData.year = 2050;
            // const request = cursor.update(updateData).onsuccess = function() {};
            // // delete
            // var request = cursor.delete().onsuccess = function() {};
          };

          // let lastPrimaryKey = getLastIteratedArticleId();
          // if (lastPrimaryKey > cursor.primaryKey) {
          //   cursor.continuePrimaryKey("javascript", lastPrimaryKey);
          //   return;
          // }
          // // update lastIteratedArticleId
          // setLastIteratedArticleId(cursor.primaryKey);
          // // preload 5 articles into the unread list;
          // unreadList.push(cursor.value);
          // if (++count < 5) {
          //   cursor.continue();
          // }

          for (x in cursor.value) {
            idb_result.innerHTML += x+': '+cursor.value[x]+' ';
          }
          idb_result.innerHTML += '';

          cursor.continue();
        } else {
          idb_result.innerHTML += 'All entries displayed';
        }
      };
    }
  

interfaces

different ways the keys are supplied

Key Path (keyPath) Key Generator (autoIncrement) Description
No No This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value.
Yes No This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path.
No Yes This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key.
Yes Yes This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key.

Workers


Dedicated Worker

Shared Worker

    var w_result = document.getElementById("w_result");

    var dw;
    function startDWorker() {
      if(!!window.Worker) { // or: typeof(Worker) !== "undefined"
        if(!dw) { dw = new Worker("js/worker-dedicated.js"); }
        dw.onmessage = function(e) {
          w_result.innerHTML = "dedicated worker: "+e.data+" "+w_result.innerHTML;
        };
        dw.port.onerror = function(e) {
          w_result.innerHTML = "dedicated worker: ERROR !";
        };
      } else {
        w_result.innerHTML += "Sorry, your browser does not support dedicated Worker...";
      }
    }
    function sendToDWorker() {
      if(dw) {
        dw.postMessage(Math.floor(Math.random() * 1000) + 1);
      }
    }
    function stopDWorker() {
      dw.terminate(); // free browser/computer resources
      // if you set the worker variable to undefined,
      // after it has been terminated, you can reuse the code
      dw = undefined;
    }

    var sw;
    function startSWorker() {
      if(!!window.SharedWorker) { // or: typeof(Worker) !== "undefined"
        if(!sw) {
          sw = new SharedWorker("js/worker-shared.js", "sharedWorker");
        }
        sw.port.onmessage = function(e) {
          w_result.innerHTML = "shared worker: "+e.data+" [lastEventId:"+e.lastEventId+"] "+w_result.innerHTML;
        };
        sw.onerror = function(e) {
          w_result.innerHTML = "shared worker: ERROR !";
          sw.port.close();
        };
        // sw.port.start(); // required when using: port.addEventListener('message',...
      } else {
        w_result.innerHTML += "Sorry, your browser does not support SharedWorker...";
      }
    }
    function sendToSWorker() {
      if(sw) {
        sw.port.postMessage(utils_randomInt());
      }
    }
    function stopSWorker() {
      if(sw) {
        sw.port.close();
      }
      sw = undefined;
    }

    // one Worker is created for each logical processor reported by the browser
    // and a record is created which includes a reference to the new worker
    // as well as a Boolean value indicating whether or not we're using that worker yet;
    // these objects are, in turn, stored into an array for later use.
    // This creates a pool of workers we can use to process requests later
    let workerList = [];
    for (let i = 0; i < window.navigator.hardwareConcurrency; i++) {
      let newWorker = {
        worker: new Worker('cpuworker.js'),
        inUse: false
      };
      workerList.push(newWorker);
    }
  

js/worker-dedicated.js


    var nr = 1, d = new Date();
    function timedCount() {
      postMessage("pulse " + (nr++)); //  post a message back to the HTML page
      setTimeout(timedCount, 1000);
    }
    timedCount();
    self.onmessage  = function(e) {
      postMessage("["+d.toLocaleDateString()+" "+d.toLocaleTimeString()+"], owner message: "+e.data);
    }
    self.onoffline = function() {
      postMessage("worker is now OFFLINE");
    }
    self.ononline = function() {
      postMessage("worker is now ONLINE");
    }
  

js/worker-shared.js


    onconnect = function(e) {
      importScripts("utils.js");
      // importScripts("pusher.worker.js");
      var client_port = e.ports[0],
          nr = 1,
          d = new Date();
      clients = [];
      clients.push(client_port); // fill list of worker clients (windows,tabs,...)
      // client_port.start();

      // // Connect to Pusher
      // var pusher = new Pusher('1fb94680701ab31a3139', {
      //     encrypted: true
      // });
      // // Subscribe to test_channel
      // var pusherChannel = pusher.subscribe('test_channel');
      // bind to 'my_event' on pusherChannel
      // pusherChannel.bind('my_event', function(data) {
      //     // Relay the payload on to each client_port
      //     clients.forEach(function(client_port){
      //         client_port.postMessage(data);
      //     });
      // });

      function timedCount() {
          // self.name
          client_port.postMessage("pulse " + (nr++)); //  post a message back to the HTML page
          setTimeout(timedCount, 1000);
      }
      timedCount();

      client_port.onmessage  = function(e) {
          client_port.postMessage("["+
              d.toLocaleDateString()+" "+d.toLocaleTimeString()+
              "], owner message: "+e.data+
              ", own message: "+utils_randomInt()+
              " pulse "+(nr++)
          );
      }
      client_port.onerror  = function(e) {
          client_port.postMessage("["+d.toLocaleDateString()+" "+d.toLocaleTimeString()+"], ERROR ! ");
      }
      client_port.onoffline = function() {
          client_port.postMessage("worker is now OFFLINE");
      }
      client_port.ononline = function() {
          client_port.postMessage("worker is now ONLINE");
      }
    }

    // var canvas = document.getElementById('myCanvas'),
    // ctx = canvas.getContext('2d'),
    // image = new Image();

    // image.onload = function() {
    //   Promise.all([
    //     createImageBitmap(image, 0, 0, 32, 32),
    //     createImageBitmap(image, 32, 0, 32, 32)
    //   ]).then(function(sprites) {
    //     ctx.drawImage(sprites[0], 0, 0);
    //     ctx.drawImage(sprites[1], 32, 32);
    //   });
    // }

    // image.src = 'sprites.png';
  

ServiceWorker (APIs)

Base Url:


Service Worker:





Append images:








    var sw_result = document.getElementById("sw_result");
    var sw_images = document.getElementById("sw_images");
    var sw_base = document.getElementById("sw_base");
    var sw_link = document.getElementById("sw_link");
    var sw_list = document.getElementById("sw_list");
    var serw;

    if ('serviceWorker' in navigator) {
      setInterval(()=>{
        sw_list.innerHTML = "";
        if (navigator.serviceWorker.controller) {
          sw_list.innerHTML += 'page is controlled by:'+
            navigator.serviceWorker.controller.scriptURL;
        } else {
          sw_list.innerHTML += 'page is not controlled by a service worker';
        }
        navigator.serviceWorker.getRegistrations().then(function(registrations){
          if(registrations.length) sw_list.innerHTML += "ServiceWorkerRegistrations found:";
          registrations.forEach((item, index, arr) => {
            var sw = item.installing || item.waiting || item.active;
            sw_list.innerHTML += "<button onclick=swUnregister('"+item.scope+
            "')>unregister "+item.scope+" ["+sw.state+"]</button>";
          });
        });
      },1000);
      navigator.serviceWorker.oncontrollerchange = function(e) {
        sw_list.innerHTML += 'page is controlled by: '+e.target.scriptURL+'';
      };
      navigator.serviceWorker.addEventListener('message', function(event){
        sw_result.innerHTML += "serviceWorker all clients message:"+event.data;
        if (event.ports[0]) {
          event.ports[0].postMessage("Hello back! to serviceWorker");
        }
      });
    } else {
      sw_result.innerHTML += 'current browser doesnt support serviceWorker';
    }

    function swUnregister (scope = "") {
      sw_result.innerHTML = "";
      navigator.serviceWorker.getRegistrations().then(function(registrations){
        registrations.forEach((item, index, arr) => {
          if(item.scope == scope){
            item.unregister().then(function(boolean) {
              if(boolean){
                sw_result.innerHTML += item.scope+" unregister is OK";
              }
              else{
                sw_result.innerHTML += item.scope+" unregister with ERROR";
              }
            });
          }
        });
      });
    }

    function installServiceWorker() {
      if  ('serviceWorker' in navigator) {
        sw_result.innerHTML = "";
        navigator.serviceWorker.register(
          sw_link.value // +"?v="+Math.random()
          // ,{ scope: sw_base.value+'/' }
          // ,{ scope: 'http://web-engineer-book' }
          // ,{ scope: sw_base.value }
          // ,{ scope: './' }
        ).then(function(reg) {
          if (reg.installing) {
            sw_result.innerHTML += 'installing serviceWorker';
            serw = reg.installing;
          } else if (reg.waiting) {
            sw_result.innerHTML += 'serviceWorker installed';
            serw = reg.waiting;
          } else if (reg.active) {
            sw_result.innerHTML += 'serviceWorker active';
            serw = reg.active;
          }
          if (serw) {

            // reg.pushManager.getSubscription().then(function(subscription) {
            //   if (!subscription) {
            //     sw_result.innerHTML += 'Not yet subscribed to Push';
            //     return;
            //   }
            //   var mySubscription = subscription.toJSON();

            //   // initialize status, which includes setting UI elements for subscribed status
            //   // and updating Subscribers list via push
            //   var endpoint = subscription.endpoint;
            //   var key = subscription.getKey('p256dh');
            //   var auth = subscription.getKey('auth');

            //   subscription.unsubscribe().then(function(successful) {
            //     // You've successfully unsubscribed
            //   }).catch(function(e) {
            //     // Unsubscription failed
            //   })

            //   // Enable any UI which subscribes/unsubscribes from
            //   // push messages.
            //   var pushButton = document.querySelector('.js-push-button');
            //   pushButton.disabled = false;
            //   if (!subscription) {
            //     // We aren’t subscribed to push, so set UI
            //     // to allow the user to enable push
            //     return;
            //   }
            //   // Keep your server in sync with the latest subscriptionId
            //   sendSubscriptionToServer(subscription);
            //   showCurlCommand(subscription);
            //   // Set your UI to show they have subscribed for
            //   // push messages
            //   pushButton.textContent = 'Disable Push Messages';
            //   isPushEnabled = true;
            // }).catch(function(err) {
            //   sw_result.innerHTML += 'Error during getSubscription()';
            // });

            sw_result.innerHTML += 'serviceWorker state: '+serw.state+'';
            sw_result.innerHTML += 'serviceWorker scriptURL: '+serw.scriptURL+'';
            serw.onstatechange = function(event) {
              sw_result.innerHTML += 'serviceWorker state changed: '+event.target.state+'';
            };
            // // same as serw.onstatechange
            // reg.onupdatefound = function() {
            //   sw_result.innerHTML += 'new service worker is being installed, scope: '+reg.scope+'';
            // };
          }
        }).catch(function(error) {
          sw_result.innerHTML += 'Registration failed with ' + error+'';
        });

        // this.onpush = function(event) {
        //   sw_result.innerHTML += 'onpush event.data: '+event.data+'';
        // }
        // navigator.serviceWorker.ready.then( function(reg) {
        //   sw_result.innerHTML += 'serviceWorker is active: '+reg.active+'';
        //   // reg.PushManager.supportedContentEncodings

        //   var options = {
        //     userVisibleOnly: true,
        //     applicationServerKey: 'qwerty654321'
        //   };
        //   reg.pushManager.subscribe(options).then(
        //     function(sbs) {
        //       // push subscription details
        //       sw_result.innerHTML += 'pushSubscription.subscriptionId: '+sbs.subscriptionId+'';
        //       sw_result.innerHTML += 'pushSubscription.endpoint: '+sbs.endpoint+'';
        //       sw_result.innerHTML += 'pushSubscription.expirationTime: '+sbs.expirationTime+'';
        //       sw_result.innerHTML += 'pushSubscription.options: '+sbs.options+'';
        //     }, function(error) {
        //       // report information about errors back to the application server
        //       sw_result.innerHTML += 'pushSubscription.error: '+error+'';
        //     }
        //   );

        // });
      }
    }

    function msgServiceWorker() {
      navigator.serviceWorker.controller.postMessage("Client 1 message 1");
      return new Promise(function(resolve, reject){
        var msg_channel = new MessageChannel();
        msg_channel.port1.onmessage = function(event){
          if(event.data.error) {
            sw_result.innerHTML += "messageChannel:"+event.data.error;
            // reject(event.data.error);
          } else {
            sw_result.innerHTML += "messageChannel:"+event.data;
            // resolve(event.data);
          }
        };
        // Send message to service worker along with port for reply
        navigator.serviceWorker.controller.postMessage("Client 1 message 2", [msg_channel.port2]);
      });
    }

    function listCache() {
      sw_result.innerHTML = "CACHES:";
      caches_obj = {};
      caches.keys().then(
        cacheNames => cacheNames
      ).then( cacheNames =>{
        cacheNames.forEach( cname => {
          caches.open(cname).then( cache => {
            cache.keys().then(ks => {
              caches_obj[cname]=ks.map(req=>req.url)
            });
          });
        });
      });
      setTimeout(()=>{
        for (cname in caches_obj) {
          sw_result.innerHTML +=
          "<button onclick=\"removeCache('"+cname+"')\">"
          +cname+" (remove)</button>";
          caches_obj[cname].forEach((url) => {
            sw_result.innerHTML += url;
          });
        }
      },500);
    }
    function removeCache(key) {
      sw_result.innerHTML = "";
      deleted = "";
      caches.keys().then(cacheNames => {
        return Promise.all(
          cacheNames.map(cname => {
            if(cname==key) {
              return caches.delete(cname);
            }
          })
        ).then( ok => {
          sw_result.innerHTML += "[ "+key+" ] cache deleted";
        }, err => {
          sw_result.innerHTML += "unable to remove cache ! "+err;
        });
      })
    }

    function addImage(url) {

      // var myImage = document.createElement("img");
      // myImage.setAttribute("height","100px");
      // myImage.src = sw_base.value+url
      // sw_result.appendChild(myImage);
      // sw_result.innerHTML += '';

      imgLoad(url).then(function(img) {
        var myImage = document.createElement("img");
        myImage.setAttribute("height","100px");
        myImage.src = window.URL.createObjectURL(img);
        sw_result.appendChild(myImage);
        sw_result.innerHTML += '';
      }, function(Error) {
        sw_result.innerHTML += 'Error';
      });
    }
    function imgLoad(link) {
      // return a promise for an image loading
      return new Promise(function(resolve, reject) {
        var request = new XMLHttpRequest();
        request.open('GET', sw_base.value+link);
        request.responseType = 'blob';
        request.onload = function() {
          if (request.status == 200) {
            resolve(request.response);
          } else {
            reject(Error('Image didnt load successfully:' + request.statusText));
          }
        };
        request.onerror = function() {
          reject(Error('There was a network error'));
        };
        request.send();
      });
    }

    function showNotification() {
      Notification.requestPermission(function(result) {
        if (result === 'granted') {
          navigator.serviceWorker.ready.then(function(registration) {
            registration.showNotification('Vibration Sample', {
              body: 'Buzz! Buzz!',
              vibrate: [200, 100, 200, 100, 200, 100, 200],
              tag: 'vibration-sample',
              icon: 'images/cellphone.jpg',
              badge: 'images/cellphone.jpg',
              image: 'images/cellphone.jpg',
              dir: 'rtl',
              data: 'random string as data '+Math.PI,
              // actions:{action:str,title:str,icon:url},
              // lang:en-US,
              // renotify:false|true,
              // requireInteraction:false|true,
            });
            var options = { tag : 'user_alerts' };
            registration.getNotifications().then(function(notifications) {
              sw_result.innerHTML = "Notifications:";
              notifications.forEach((n)=>{
                sw_result.innerHTML += "body: "+n.body;
                sw_result.innerHTML += "data: "+n.data;
                sw_result.innerHTML += "dir: "+n.dir;
                sw_result.innerHTML += "icon: "+n.icon;
                sw_result.innerHTML += "lang: "+n.lang;
                sw_result.innerHTML += "tag: "+n.tag;
                sw_result.innerHTML += "title: "+n.title;
                sw_result.innerHTML += "-----------------------";
                // sw_result.innerHTML += "onclick: "+n.onclick;
                // sw_result.innerHTML += "onclose: "+n.onclose;
                // sw_result.innerHTML += "onerror: "+n.onerror;
                // sw_result.innerHTML += "onshow: "+n.onshow;
              })
            })
          });
        }
      });
    }
  

js-service-worker.js


    const CACHE_VERSION = 1;
    const CURRENT_CACHES = {
      prefetch: 'v' + CACHE_VERSION,
      // other_cache: 'v' + OTHER_CACHE_VERSION
    };
    const cached_key='cached';

    self.addEventListener('install', event => {
      var now = Date.now();
      var urlsToPrefetch = [
        // 'index.html',
        // './', // Alias for index.html
        'images/wide.jpg',
        'images/sw-lifecycle.png',
        'images/rock.jpg',
        'images/workplace.jpg',
        'images/paris.jpg',
        'images/mac.jpg'
      ];
      event.waitUntil(
        // caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
        //   return cache.addAll(urlsToPrefetch);
        // });
        caches.open(CURRENT_CACHES.prefetch).then(cache => {

          var cachePromises = urlsToPrefetch.map(urlToPrefetch => {
            var url = new URL(urlToPrefetch, location.href);
            url.search += (url.search?'&':'?')+cached_key+'=' + now;

            // if there is any chance that the resources
            // are served off of a server that doesnt support CORS
            var request = new Request(url,{mode:'no-cors'});

            return fetch(request).then(response => {
              if (response.status >= 400) {
                throw new Error('request for ' + urlToPrefetch +
                  ' failed with status ' + response.statusText);
              }
              // Use the original URL without the cache-busting parameter as the key for cache.put().
              return cache.put(urlToPrefetch, response);
            }).catch(error => {
              console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
            });
          });
          return Promise.all(cachePromises).then(() => {
            // console.log(CURRENT_CACHES.prefetch+' prefetched');
          });
        })
        // .then(self.skipWaiting())
        .catch(error => {
          console.error('prefetching failed:', error);
        })
        // caches.has('v1').then(function(hasCache) {
        //   if (!hasCache) {
        //     someCacheSetupfunction();
        //   } else {
        //     caches.open('v1').then(function(cache) {
        //       return cache.addAll(myAssets);
        //     });
        //   }
        // }).catch(function() {
        //   // Handle exception here.
        // });
      );
    });

    self.addEventListener('activate', event => {
      event.waitUntil( async function() {
        // clients loaded in the same scope do not need to be reloaded
        // before their fetches will go through this service worker :
        clients.claim();

        // delete all caches that aren't named in CURRENT_CACHES
        var expectedCacheNames = Object.values(CURRENT_CACHES);
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames.map(cacheName => {
              if (expectedCacheNames.indexOf(cacheName) === -1) {
                // If this cache name isn't present in the array of "expected" cache names, then delete it
                console.log('deleting out of date cache:', cacheName);
                return caches.delete(cacheName);
              }
            })
          );
        })

        // // enable navigation preloads
        // if (self.registration.navigationPreload) {
        //   await self.registration.navigationPreload.enable();
        // }
      }());
    });

    self.addEventListener('fetch', event => {
      // avoid non-GET requests
      if ( event.request.method != 'GET') return;
      // && event.request.headers.get('accept').indexOf('text/html') !== -1

      event.waitUntil(async function() {
        // Exit early if we don't have access to the client.
        // Eg, if it's cross-origin.
        if (!event.clientId) return;
        // Get the client.
        const client = await clients.get(event.clientId);
        // Exit early if we don't get the client.
        // Eg, if it closed.
        if (!client) return;
        // Send a message to the client.
        self.clients.matchAll().then(function (clients) {
          clients.forEach(function(client) {
            // client.postMessage(
            // "Hey ["+event.clientId+"],
            // serviceWorker is fetching or looking in cache...");
          });
        });
      }());

      event.respondWith(caches.match(event.request).then(r => {
        // console.log( r ? 'cache: '+r.url : 'no cache, need to fetch...' );
        return r || fetch(event.request).then(response => {
          caches.open(CURRENT_CACHES.prefetch).then(cache => {
            cache.put(event.request, response);
          });
          console.log('fetched: ', response.url);
          return response.clone();
          // return cache.put(event.request, response.clone()).then(() => {
          //   return response;
          // });
        }).catch(() => {
          // return caches.match('/sw-test/gallery/myLittleVader.jpg');
          console.error('no cache, fetching failed: ', error);
          throw error;
        });
      }));

      // // fallback response
      // var response;
      // event.respondWith(caches.match(event.request).catch(function() {
      //   return fetch(event.request);
      // }).then(function(r) {
      //   response = r;
      //   caches.open('v1').then(function(cache) {
      //     cache.put(event.request, response);
      //   });
      //   return response.clone();
      // }).catch(function() {
      //   return caches.match('/sw-test/gallery/myLittleVader.jpg');
      // }));
    });

    function send_message_to_all_clients(msg){
      clients.matchAll().then(clients => {
        clients.forEach(client => {
          send_message_to_client(client, msg).then(m => console.log(m));
        })
      })
    }
    function send_message_to_client(client, msg){
      return new Promise(function(resolve, reject){
        var msg_channel = new MessageChannel();
        msg_channel.port1.onmessage = function(event){
          if(event.data.error){
            reject(event.data.error);
          }else{
            resolve(event.data);
          }
        };
        client.postMessage("serviceWorker message: >>>"+msg+"<<<", [msg_channel.port2]);
      });
    }
    self.addEventListener('message', event => {
      if (event.ports[0]) { // client channel messaging
        event.ports[0].postMessage("Hello back! to serviceWorker client ["+event.data+"]");
      }
      // response to any message
      send_message_to_all_clients("serviceWorker received message: " + event.data);
    });

    self.addEventListener('notificationclick', function(event) {
      event.notification.close();

      send_message_to_all_clients("client notification data: " + event.notification.data);

      self.registration.showNotification("google.com was opened (click to reopen)", {
        actions: [{
          action: "get",
          title: "Get now.",
          icon: 'images/cellphone.jpg'
        }],
        body: 'Buzz! Buzz!',
        vibrate: [200, 100, 200, 100, 200, 100, 200],
        tag: 'vibration-sample',
        icon: 'images/cellphone.jpg',
        badge: 'images/cellphone.jpg',
        image: 'images/cellphone.jpg',
        dir: 'ltr',
        data: 'serviceWorker notification data'
        // lang:en-US,
        // renotify:false|true,
        // requireInteraction:false|true,
        // vibrate:pattern,
        // data:data
      });

      // focus on domain window or open
      event.waitUntil(
        clients.matchAll({
          type: "window"
        }).then(function(clientList) {
          for (var i = 0; i < clientList.length; i++) {
            var client = clientList[i];
            if (
              client.url == 'https://www.google.com/' &&
              'focus' in client
            ) {
              if(!client.focused) return client.focus();
            }
          }
          if (clients.openWindow) {
            return clients.openWindow('https://www.google.com/');
          }
        }));
      }
    );

    self.addEventListener('push', function(event) {
      if (!(self.Notification && self.Notification.permission === 'granted')) {
        return;
      }
      var data = {};
      if (event.data) {
        data = event.data.json();
        // event.data.text();
        // event.data.blob();
        // event.data.arrayBuffer();
      }
      if(data.action === 'subscribe' || data.action === 'unsubscribe') {
        var title = data.title || "Something Has Happened";
        var message = data.message || "Here's something you might want to check out.";
        // var icon = "../images/new-notification.png";
        var notification = new Notification(title, {
          body: message,
          tag: 'simple-push-demo-notification'
          // , icon: icon
        });
        notification.addEventListener('click', function() {
          if (clients.openWindow) {
            // clients.openWindow('https://example.blog.com/2015/03/04/something-new.html');
          }
        });
        port.postMessage(data);
      } else if(data.action === 'init' || data.action === 'chatMsg') {
        port.postMessage(data);
      }

      event.waitUntil(
        self.registration.showNotification('Yay a message.', {
          body: 'We have received a push message.',
          icon: '/images/icon-192x192.png',
          tag: 'simple-push-demo-notification-tag'
        })
      );
      // var dataInit = {
      //   data : 'Some sample text'
      // }
      // var myPushEvent = new PushEvent('push', dataInit);
      // myPushEvent.data.text(); // should return 'Some sample text'
    });

    self.addEventListener('pushsubscriptionchange', function() {
      // do something, usually resubscribe to push and
      // send the new subscription details back to the
      // server via XHR or Fetch
    });
  

Messaging


    // inside main window/worker/frame
    var channel = new MessageChannel();
    var msg_frame = document.getElementById("msg_frame"); // target client
    msg_frame.addEventListener("load", () => {
      channel.port1.onmessage = (e) => {
        sw_result.innerHTML += "iFrame message: "+e.data;
      };
      msg_frame.contentWindow.postMessage('Hello from the main page!', '*', [channel.port2]);
    });
    // target iframe
    iframe
      id="msg_frame"
      scr="http://web-engineer-book/js-api-msg-iframe.html"
      name="msg_frame"
      style="margin-left:1em;width:98%;height:50px;overflow:auto;font-size:1em;border:3px dotted gray;"
    /iframe

    // inside client window/worker/frame
    var output = document.querySelector('.output');
    window.addEventListener('message', (e) => {
      output.innerHTML = e.data;
      // Use the transfered port to post a message back to the main frame
      e.ports[0].postMessage('Message back from the IFrame');
    });
  

Download Progress


    // ----- WITH FETCH

    class ProgressReportFetcher {
      constructor(onProgress = function() {}) {
        this.onProgress = onProgress;
      }
      // mimic native fetch() instantiation and return Promise
      fetch(input, init = {}) {
        const request = (input instanceof Request)? input : new Request(input)
        this._cancelRequested = false;

        return fetch(request, init).then(response => {
          if (!response.body) {
            throw Error('ReadableStream is not yet supported in this browser.')
          }
          // this occurs if cancel() was called before server responded (before fetch() Promise resolved)
          if (this._cancelRequested) {
            response.body.getReader().cancel();
            return Promise.reject('cancel requested before server responded.');
          }
          if (!response.ok) {
            // HTTP error server response
            throw Error(`Server responded ${response.status} ${response.statusText}`);
          }
          // Server must send CORS header "Access-Control-Expose-Headers: content-length" to access
          const contentLength = response.headers.get('content-length');
          if (contentLength === null) {
            // don't evaluate download progress if we can't compare against a total size
            // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Access-Control-Expose-Headers
            throw Error('Content-Length server response header missing')
          }
          const total = parseInt(contentLength,10);
          let loaded = 0;
          this._reader=response.body.getReader()
          const me = this;
          return new Response(
            new ReadableStream({
              start(controller) {
                if (me.cancelRequested) {
                  console.log('canceling read')
                  controller.close();
                  return;
                }
                read();
                function read() {
                  me._reader.read().then(({done, value}) => {
                    if (done) {
                      // ensure onProgress called when content-length=0
                      if (total === 0) {
                        me.onProgress.call(me, {loaded, total});
                      }
                      controller.close();
                      return;
                    }
                    loaded += value.byteLength;
                    me.onProgress.call(me, {loaded, total});
                    controller.enqueue(value);
                    read();
                  }).catch(error => {
                    console.error(error);
                    controller.error(error)
                  });
                }
              }
            })
          )
        });
      }
      cancel() {
        console.log('download cancel requested.')
        this._cancelRequested = true;
        if (this._reader) {
          console.log('cancelling current download');
          return this._reader.cancel();
        }
        return Promise.resolve();
      }
    }
    const imageLoader = (function() {
      const loader = document.getElementById('loader');
      const img = loader.querySelector('img');
      const errorMsg = loader.querySelector('.error');
      const loading = loader.querySelector('.progress-bar');
      const progress = loader.querySelector('.progress');
      let locked, started, progressFetcher, pct;
      function downloadDone(url) {
        console.log('downloadDone()')
        img.src=url;
        img.offsetWidth; // pre-animation enabler
        loader.classList.remove('loading');
        loader.classList.add('loading-complete');
        // progressFetcher = null;
      }
      function startDownload() {
        // Ensure "promise-safe" (aka "thread-safe" JavaScript).
        // Caused by slow server response or consequetive calls to startDownload()
        // before stopDownload() Promise resolves
        if (locked) {
          console.error('startDownload() failed. Previous download not yet initialized');
          return;
        }
        locked = true;
        stopDownload()
        .then(function() {
          locked = false;
          progress.style.transform=`scaleX(0)`;
          progress.offsetWidth; /* prevent animation when set to zero */
          started = false;
          pct = 0;
          loader.classList.add('loading');
          loader.classList.remove('loading-complete');
          if (!progressFetcher) {
            progressFetcher = new ProgressReportFetcher(updateDownloadProgress);
          }
          console.log('Starting download...');
          progressFetcher.fetch(
            'https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg')
          .then(response => response.blob())
          .then(blob => URL.createObjectURL(blob))
          .then(url => downloadDone(url))
          .catch(error => showError(error))
        });
      }
      function stopDownload() {
        // stop previous download
        if (progressFetcher) {
          return progressFetcher.cancel()
        } else {
          // no previous download to cancel
          return Promise.resolve();
        }
      }
      function showError(error) {
        console.error(error);
        loader.classList.remove('loading');
        loader.classList.remove('loading-complete');
        loader.classList.remove('loading-error');
        errorMsg.offsetWidth; // pre-animation enabler
        errorMsg.innerHTML = 'ERROR: '+ error.message;
        loader.classList.add('loading-error');
      }
      function updateDownloadProgress({loaded, total}) {
        if (!started) {
          loader.classList.add('loading');
          started = true;
        }

        // handle divide-by-zero edge case when Content-Length=0
        pct = total? loaded/total : 1;

        progress.style.transform=`scaleX(${pct})`;
        // console.log('downloaded', Math.round(pct*100)+'%')
        if (loaded === total) {
          console.log('download complete')
        }
      }
      return {
        startDownload,
        stopDownload
      }
    })()
    imageLoader.startDownload();

    // ----- WITH SERVICE WORKER

    self.addEventListener('fetch', event => {
      const url = new URL(event.request.url);
      const scope = self.registration.scope;
      // redirect index.html to service-worker-enabled page
      if (event.request.url === scope || event.request.url === scope+'index.html') {
        const newUrl = scope+'sw-installed.html';
        console.log('respondWith', newUrl);
        event.respondWith(fetch(newUrl))
      } else if (progressIndicatorUrls.test(event.request.url)) {
        console.log('VER',2,event.request.url)
        event.respondWith(fetchWithProgressMonitor(event))
      }
    })
    function fetchWithProgressMonitor(event) {
      /*  opaque request responses won't give us access to Content-Length and
      *  Response.body.getReader(), which are required for calculating download
      *  progress.  Respond with a newly-constructed Request from the original Request
      *  that will give us access to those.
      *  See https://stackoverflow.com/questions/39109789/what-limitations-apply-to-opaque-responses
      *  'Access-Control-Allow-Origin' header in the response must not be the
      *  wildcard '*' when the request's credentials mode is 'include'.  We'll omit credentials in this demo.
      */
      const newRequest = new Request(event.request, {
        mode: 'cors',
        credentials: 'omit'
      })
      return fetch(newRequest).then(response => respondWithProgressMonitor(event.clientId, response));
    }
    function respondWithProgressMonitor(clientId, response) {
      if (!response.body) {
        // See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
        console.warn("ReadableStream is not yet supported in this browser")
        return response;
      }
      if (!response.ok) {
        // HTTP error code response
        return response;
      }
      const contentLength = response.headers.get('content-length');
      if (contentLength == null) {
        // don't track download progress if we can't compare against a total size
        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Access-Control-Expose-Headers
        console.warn('No Content-Length no header in response.');
        return response;
      }
      let loaded = 0;
      debugReadIterations=0; // direct correlation to server's response buffer size
      const total = parseInt(contentLength,10);
      const reader = response.body.getReader();
      return new Response(
        new ReadableStream({
          start(controller) {
            // get client to post message. Awaiting resolution first read() progress
            // is sent for progress indicator accuracy
            let client;
            clients.get(clientId).then(c => {
              client = c;
              read();
            });
            function read() {
              debugReadIterations++;
              reader.read().then(({done, value}) => {
                if (done) {
                  console.log('read()', debugReadIterations);
                  controller.close();
                  return;
                }
                controller.enqueue(value);
                loaded += value.byteLength;
                // console.log('    SW', Math.round(loaded/total*100)+'%');
                dispatchProgress({client, loaded, total});
                read();
              })
              .catch(error => {
                // error only typically occurs if network fails mid-download
                console.error('error in read()', error);
                controller.error(error)
              });
            }
          },
          // Firefox excutes this on page stop, Chrome does not
          cancel(reason) {
            console.log('cancel()', reason);
          }
        })
      )
    }
    function dispatchProgress({client, loaded, total}) {
      client.postMessage({loaded,total})
    }
  

ServiceWorker interfaces (available properties/methods)

Push API

Streams









    <script src="../js/pako.min.js"></script>
    <script src="../js/crc32.js"></script>
    <script src="../js/png-lib.js"></script>
    <script src="../js/png-transform-stream.js"></script>
    <script src="../js/png-chunks.js"></script>
  

    var streams_tests = document.getElementById("streams_tests");

    function streamInImage (grayscale = false) {
      streams_tests.innerHTML = "";
      const image = document.createElement("img");
      image.setAttribute("height","50px");
      // Fetch the original image
      fetch('images/gta5.png') // process rgb-alpha for conversion
      // Retrieve its body as ReadableStream
      .then(response => response.body)
      .then(rs => {
        if (grayscale) {
          return rs.pipeThrough(new TransformStream(new GrayscalePNGTransformer()))
        }
        // read as stream
        const reader = rs.getReader();
        return new ReadableStream({
          async start(controller) {
            while (true) {
              const { done, value } = await reader.read();
              // When no more data needs to be consumed, break the reading
              if (done) { break; }
              // Enqueue the next data chunk into our target stream
              controller.enqueue(value);
            }
            // Close the stream
            controller.close();
            reader.releaseLock();
          }
        })
      })
      // Create a new response out of the stream
      .then(rs => new Response(rs))
      // Create an object URL for the response
      .then(response => response.blob())
      .then(blob => URL.createObjectURL(blob))
      // Update image
      .then(url => {
        image.src = url
        streams_tests.appendChild(image);
      }).catch(console.error);
    }

    var rss_result = "";
    function randomStringStreamSTART () {
      streams_tests.innerHTML = "";
      result = "";
      const stream = new ReadableStream({
        start(controller) {
          interval = setInterval(() => {
            let string = randomChars();
            // Add the string to the stream
            controller.enqueue(string);
            // show it on the screen
            streams_tests.innerHTML += string;
          }, 1000);
          document.getElementById("rssc").addEventListener('click', function() {
            clearInterval(interval);

            // function teeStream() {
            //   const teedOff = stream.tee();
            //   readStream(teedOff[0], list2);
            //   readStream(teedOff[1], list3);
            // }
            // function readStream(stream, list) {

            // readStream() - START
            const reader = stream.getReader();
            let charsReceived = 0;
            // read() returns a promise that resolves
            // when a value has been received
            reader.read().then(function processText({ done, value }) {
              // Result objects contain two properties:
              // done  - true if the stream has already given you all its data.
              // value - some data. Always undefined when done is true.
              if (done) {
                streams_tests.innerHTML += "Stream complete: "+rss_result;
                return;
              }
              charsReceived += value.length;
              const chunk = value;
              streams_tests.innerHTML += 'Read ' + charsReceived +
                  ' characters so far. Current chunk = '+chunk;
              rss_result += chunk;
              // Read some more, and call this function again
              return reader.read().then(processText);
            });
            // readStream() - END
            controller.close();
          })
        },
        pull(controller) {
          // We don't really need a pull in this example
        },
        cancel() {
          // This is called if the reader cancels,
          // so we should stop generating strings
          clearInterval(interval);
        }
      });
    }
    function randomChars() {
      let string = "";
      let choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
      for (let i = 0; i < 8; i++) {
        string += choices.charAt(Math.floor(Math.random() * choices.length));
      }
      return string;
    }

    function sendMessage(message) {
      streams_tests.innerHTML = "";
      const decoder = new TextDecoder("utf-8");
      const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });
      let result = "";
      const writableStream = new WritableStream({
        // Implement the sink
        write(chunk) {
          return new Promise((resolve, reject) => {
            var buffer = new ArrayBuffer(2);
            var view = new Uint16Array(buffer);
            view[0] = chunk;
            var decoded = decoder.decode(view, { stream: true });
            streams_tests.innerHTML += "Chunk decoded: "+decoded;
            result += decoded;
            resolve();
          });
        },
        close() { streams_tests.innerHTML += "[MESSAGE RECEIVED] " + result; },
        abort(err) { streams_tests.innerHTML += "Sink error: " + err; }
      }, queuingStrategy);

      // defaultWriter is of type WritableStreamDefaultWriter
      const defaultWriter = writableStream.getWriter();
      const encoder = new TextEncoder();
      const encoded = encoder.encode(message, { stream: true });
      encoded.forEach((chunk) => {
        defaultWriter.ready
          .then(() => {
            return defaultWriter.write(chunk);
          })
          .then(() => {
            // streams_tests.innerHTML += "Chunk written to sink";
          })
          .catch((err) => {
            streams_tests.innerHTML += "Chunk error: "+err;
          });
      });
      // Call ready again to ensure that all chunks are written
      //   before closing the writer.
      defaultWriter.ready
        .then(() => {
          defaultWriter.close();
        })
        .then(() => {
          // streams_tests.innerHTML += "All chunks written";
        })
        .catch((err) => {
          streams_tests.innerHTML += "Stream error: "+err;
        });
    }

    function PngChunksStreamLog (img) {
      streams_tests.innerHTML = "";
      // Fetch the original image
      fetch(img)
      // Retrieve its body as ReadableStream
      .then(response => response.body)
      // Log each fetched Uint8Array chunk
      .then(rs => logReadableStream('Fetch Response Stream', rs))
      // Transform to a PNG chunk stream
      .then(rs => rs.pipeThrough(new PNGTransformStream()))
      // Log each transformed PNG chunk
      .then(rs => logReadableStream('PNG Chunk Stream', rs))
    }
    class LogStreamSink {
      constructor(name) {
        this.name = name;
        this.counter = 0;
      }
      write(chunk) {
        this.counter += 1;
        // console.log('Chunk %d of %s: %o', this.counter, this.name, chunk);
        this.createRow(this.name, this.counter, chunk.constructor.name);
      }
      close() {
        this.createRow(this.name, this.counter, 'Closed');
      }
      createRow(heading, col1, col2) {
        streams_tests.innerHTML += heading+" "+col1+" "+col2;
      }
    }
    function logReadableStream(name, rs) {
      const [rs1, rs2] = rs.tee();
      rs2.pipeTo(new WritableStream(new LogStreamSink(name))).catch(console.error);
      return rs1;
    }
  

PWA

Structure

HTML


    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>js13kGames A-Frame entries</title>
      <meta name="description" content="A list ...">
      <meta name="author" content="end3r">
      <meta name="theme-color" content="#B12A34">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <meta property="og:image" content="icons/icon-512.png">
      <link rel="shortcut icon" href="favicon.ico">
      <link rel="stylesheet" href="style.css">
      <link rel="manifest" href="js13kpwa.webmanifest">
      <script src="data/games.js" defer></script>
      <script src="app.js" defer></script>
    </head>
    <body>
    <header>
      <p>
        <a class="logo" href="http://js13kgames.com">
        <img src="img/js13kgames.png" alt="js13kGames">
        </a>
      </p>
    </header>
    <main>
      <h1>js13kGames A-Frame entries</h1>
      <p class="description">....</p>
      <button id="notifications">notifications</button>
      <section id="content">
        // Content inserted in here
      </section>
    </main>
    <footer>
      <p>©....</p>
    </footer>
    </body>
    </html>
  

app.js


    var template = "<article>\n\
      <img src='data/img/SLUG.jpg' alt='NAME'>\n\
      <h3>#POS. NAME</h3>\n\
      <ul>\n\
      <li><span>Author:</span> <strong>AUTHOR</strong></li>\n\
      <li><span>Twitter:</span> <a href='https://twitter.com/TWITTER'>@TWITTER</a></li>\n\
      <li><span>Website:</span> <a href='http://WEBSITE/'>WEBSITE</a></li>\n\
      <li><span>GitHub:</span> <a href='https://GITHUB'>GITHUB</a></li>\n\
      </ul>\n\
    </article>";
    var content = '';
    for(var i=0; i<games.length; i++) {
      var entry = template.replace(/POS/g,(i+1))
        .replace(/SLUG/g,games[i].slug)
        .replace(/NAME/g,games[i].name)
        .replace(/AUTHOR/g,games[i].author)
        .replace(/TWITTER/g,games[i].twitter)
        .replace(/WEBSITE/g,games[i].website)
        .replace(/GITHUB/g,games[i].github);
      entry = entry.replace('<a href=\'http:///\'></a>','-');
      content += entry;
    };
    document.getElementById('content').innerHTML = content;

    if('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
    };

    var button = document.getElementById("notifications");
    button.addEventListener('click', function(e) {
      Notification.requestPermission().then(function(result) {
        if(result === 'granted') {
          randomNotification();
        }
      });
    });

    function randomNotification() {
      var randomItem = Math.floor(Math.random()*games.length);
      var notifTitle = games[randomItem].name;
      var notifBody = 'Created by '+games[randomItem].author+'.';
      var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
      var options = {
        body: notifBody,
        icon: notifImg
      }
      var notif = new Notification(notifTitle, options);
      setTimeout(randomNotification, 30000);
    }
  

sw.js


    self.importScripts('data/games.js');

    var cacheName = 'js13kPWA-v1';
    var appShellFiles = [
      '/pwa-examples/js13kpwa/',
      '/pwa-examples/js13kpwa/index.html',
      '/pwa-examples/js13kpwa/app.js',
      '/pwa-examples/js13kpwa/style.css',
      '/pwa-examples/js13kpwa/fonts/graduate.eot',
      '/pwa-examples/js13kpwa/fonts/graduate.ttf',
      '/pwa-examples/js13kpwa/fonts/graduate.woff',
      '/pwa-examples/js13kpwa/favicon.ico',
      '/pwa-examples/js13kpwa/img/js13kgames.png',
      '/pwa-examples/js13kpwa/img/bg.png',
      '/pwa-examples/js13kpwa/icons/icon-32.png',
      '/pwa-examples/js13kpwa/icons/icon-64.png',
      '/pwa-examples/js13kpwa/icons/icon-96.png',
      '/pwa-examples/js13kpwa/icons/icon-128.png',
      '/pwa-examples/js13kpwa/icons/icon-168.png',
      '/pwa-examples/js13kpwa/icons/icon-192.png',
      '/pwa-examples/js13kpwa/icons/icon-256.png',
      '/pwa-examples/js13kpwa/icons/icon-512.png'
    ];
    var gamesImages = [];
    for(var i=0; i<games.length; i++) {
      gamesImages.push('data/img/'+games[i].slug+'.jpg');
    }
    var contentToCache = appShellFiles.concat(gamesImages);

    self.addEventListener('install', function(e) {
      console.log('[Service Worker] Install');
      e.waitUntil(
        caches.open(cacheName).then(function(cache) {
          console.log('[Service Worker] Caching all: app shell and content');
          return cache.addAll(contentToCache);
        })
      );
    });

    self.addEventListener('activate', function(e) {
      e.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(keyList.map(function(key) {
            if(cacheName.indexOf(key) === -1) {
              return caches.delete(key);
            }
          }));
        })
      );
    });

    self.addEventListener('fetch', function(e) {
      e.respondWith(
        caches.match(e.request).then(function(r) {
          console.log('[Service Worker] Fetching resource: '+e.request.url);
          return r || fetch(e.request).then(function(response) {
            return caches.open(cacheName).then(function(cache) {
              console.log('[Service Worker] Caching new resource: '+e.request.url);
              cache.put(e.request, response.clone());
              return response;
            });
          });
        })
      );
    });
  

games.js


    var games = [
      {
        slug: 'lost-in-cyberspace',
        name: 'Lost in Cyberspace',
        author: 'Zosia and Bartek',
        twitter: 'bartaz',
        website: '',
        github: 'github.com/bartaz/lost-in-cyberspace'
      },
      {
        slug: 'vernissage',
        name: 'Vernissage',
        author: 'Platane',
        twitter: 'platane_',
        website: 'github.com/Platane',
        github: 'github.com/Platane/js13k-2017'
      },
      // ...
      {
        slug: 'emma-3d',
        name: 'Emma-3D',
        author: 'Prateek Roushan',
        twitter: '',
        website: '',
        github: 'github.com/coderprateek/Emma-3D'
      }
    ];
  

js13kpwa.webmanifest


    {
      "name": "js13kGames Progressive Web App",
      "short_name": "js13kPWA",
      "description": "Progressive Web App that lists ...",
      "icons": [
        {
          "src": "icons/icon-32.png",
          "sizes": "32x32",
          "type": "image/png"
        },
        {
          "src": "icons/icon-64.png",
          "sizes": "64x64",
          "type": "image/png"
        },
        {
          "src": "icons/icon-96.png",
          "sizes": "96x96",
          "type": "image/png"
        },
        {
          "src": "icons/icon-128.png",
          "sizes": "128x128",
          "type": "image/png"
        },
        {
          "src": "icons/icon-168.png",
          "sizes": "168x168",
          "type": "image/png"
        },
        {
          "src": "icons/icon-192.png",
          "sizes": "192x192",
          "type": "image/png"
        },
        {
          "src": "icons/icon-256.png",
          "sizes": "256x256",
          "type": "image/png"
        },
        {
          "src": "icons/icon-512.png",
          "sizes": "512x512",
          "type": "image/png"
        }
      ],
      "start_url": "/pwa-examples/js13kpwa/index.html",
      "scope": "/myapp/"
      // "start_url": "http://web-engineer-book/index.html",
      // "scope": "http://web-engineer-book/",
      "display": "fullscreen",
      "theme_color": "#B12A34",
      "background_color": "#B12A34",
      "lang": "en-US",
      "related_applications": [
        {
          "platform": "play",
          "url": "https://play.google.com/store/apps/details?id=com.example.app1",
          "id": "com.example.app1"
        }, {
          "platform": "itunes",
          "url": "https://itunes.apple.com/app/example-app1/id123456789"
        }],
        "orientation": "landscape"
    }
  

INSTALL AS APP

js


    let deferredPrompt;
    const install_button = document.getElementById('install_button');
    install_button.style.display = 'none';
    window.addEventListener('beforeinstallprompt', (e) => {
      // Prevent Chrome 67 and earlier from automatically showing the prompt
      // e.preventDefault();
      // Stash the event so it can be triggered later.
      deferredPrompt = e;
      // Update UI to notify the user they can add to home screen
      install_button.style.display = 'block';

      install_button.addEventListener('click', (e) => {
        // hide our user interface that shows our A2HS button
        install_button.style.display = 'none';
        // Show the prompt
        deferredPrompt.prompt();
        // Wait for the user to respond to the prompt
        deferredPrompt.userChoice.then((choiceResult) => {
            if (choiceResult.outcome === 'accepted') {
              alert('User accepted the A2HS prompt');
            } else {
              alert('User dismissed the A2HS prompt');
            }
            deferredPrompt = null;
          });
      });

    });

    window.addEventListener("appinstalled", (ev) => {
      const date = new Date(ev.timeStamp / 1000);
      alert(`App got installed at ${date.toTimeString()}.`);
    });
  

PROGRESSIVE CONTENT LOADING

html


    <img src='data/img/placeholder.png' data-src='data/img/SLUG.jpg' alt='NAME'>
  

css


    article img[data-src] {
      filter: blur(0.2em);
    }
    article img {
      filter: blur(0em);
      transition: filter 0.5s;
    }
  

js


    var imagesToLoad = document.querySelectorAll('img[data-src]');
    var loadImages = function(image) {
      image.setAttribute('src', image.getAttribute('data-src'));
      image.onload = function() {
        image.removeAttribute('data-src');
      };
    };
    if('IntersectionObserver' in window) {
      var observer = new IntersectionObserver(function(items, observer) {
        items.forEach(function(item) {
          if(item.isIntersecting) {
            loadImages(item.target);
            observer.unobserve(item.target);
          }
        });
      });
      imagesToLoad.forEach(function(img) {
        observer.observe(img);
      });
    } else {
      imagesToLoad.forEach(function(img) {
        loadImages(img);
      });
    }
  

ONLINE or OFFLINE ?


    window.addEventListener('load', function() {
      var status = document.getElementById("status");
      var log = document.getElementById("log");
      function updateOnlineStatus(event) {
        var condition = navigator.onLine ? "online" : "offline";
        status.className = condition;
        status.innerHTML = condition.toUpperCase();
        log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
      }
      window.addEventListener('online',  updateOnlineStatus);
      window.addEventListener('offline', updateOnlineStatus);
    });
  

Page Observers


    var po_tests = document.getElementById("po_tests");
    var po_tests_1 = document.getElementById("po_tests_1");
    var po_tests_2 = document.getElementById("po_tests_2");

    var prevRatio = 0.0; // to save visibility
    var increasingColor = "rgba(67, 244, 65, ratio)";
    var decreasingColor = "rgba(65, 145, 244, ratio)";

    // var po_track_started = false;
    var po_is_visible = false;
    var po_view_start = 0;
    var po_total_view = 0;
    var po_data_1 = document.getElementById("po_data_1");
    var po_data_2 = document.getElementById("po_data_2");

    function startTracking() {
      track_interval = setInterval(()=>{
        let totalSeconds = po_total_view / 1000;
        let sec = Math.floor(totalSeconds % 60);
        let min = Math.floor(totalSeconds / 60);
        po_data_1.innerHTML = po_data_2.innerHTML =
          "po_is_visible: "+po_is_visible+
          "po_total_view: "+min+":"+sec.toString().padStart(2,"0")+" ["+po_total_view+"ms]";
          // "performance.now(): " + performance.now();
          updateAdTimer();
      },150);
    }
    function stopTracking() {
      clearInterval(track_interval);
    }

    // window.addEventListener("load", function(event) {
      var observer;
      var options = {
        root: null,
        rootMargin: "0px",
        threshold: buildThresholdList()
        // [0.5],
        // [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
        // [0, 0.25, 0.5, 0.75, 1.0]
      };
      observer = new IntersectionObserver(
        handleIntersect,
        options
      );
      observer.observe(po_tests);
    // }, false);

    function buildThresholdList() {
      var thresholds = [];
      var numSteps = 20; // how many thresholds between 0.0 and 1.0
      for (var i=1.0; i<=numSteps; i++) {
        var ratio = i/numSteps;
        thresholds.push(ratio);
      }
      thresholds.push(0);
      return thresholds;
    }
    function handleIntersect(entries, observer) {
      entries.forEach(function(entry) {
        po_is_visible = false;
        if (entry.isIntersecting) {
          if (entry.intersectionRatio >= 0.35) {
            po_view_start = performance.now();
            // po_track_started = true;
            po_is_visible = true;
          }
        }

        po_tests_1.innerHTML =
          "intersectionRatio: " + entry.intersectionRatio +
          "isIntersecting: " + entry.isIntersecting +
          "---------------------------------" +
          "boundingClientRect.width: " + entry.boundingClientRect.width +
          "boundingClientRect.height: " + entry.boundingClientRect.height +
          "boundingClientRect.top: " + entry.boundingClientRect.top +
          "boundingClientRect.right: " + entry.boundingClientRect.right +
          "boundingClientRect.bottom: " + entry.boundingClientRect.bottom +
          "boundingClientRect.left: " + entry.boundingClientRect.left +
          "boundingClientRect.x: " + entry.boundingClientRect.x +
          "boundingClientRect.y: " + entry.boundingClientRect.y;

        po_tests_2.innerHTML =
          "intersectionRect.width: " + entry.intersectionRect.width +
          "intersectionRect.height: " + entry.intersectionRect.height +
          "intersectionRect.top: " + entry.intersectionRect.top +
          "intersectionRect.right: " + entry.intersectionRect.right +
          "intersectionRect.bottom: " + entry.intersectionRect.bottom +
          "intersectionRect.left: " + entry.intersectionRect.left +
          "intersectionRect.x: " + entry.intersectionRect.x +
          "intersectionRect.y: " + entry.intersectionRect.y +
          "---------------------------------" +
          "rootBounds.width: " + entry.rootBounds.width +
          "rootBounds.height: " + entry.rootBounds.height +
          "rootBounds.top: " + entry.rootBounds.top +
          "rootBounds.right: " + entry.rootBounds.right +
          "rootBounds.bottom: " + entry.rootBounds.bottom +
          "rootBounds.left: " + entry.rootBounds.left +
          "rootBounds.x: " + entry.rootBounds.x +
          "rootBounds.y: " + entry.rootBounds.y;

        if (entry.intersectionRatio > prevRatio) {
          entry.target.style.backgroundColor =
            increasingColor.replace("ratio", entry.intersectionRatio);
        } else {
          entry.target.style.backgroundColor =
            decreasingColor.replace("ratio", entry.intersectionRatio);
        }
        prevRatio = entry.intersectionRatio;
        let box = entry.target;
        let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%";
        box.querySelector(".topLeft").innerHTML = visiblePct;
        box.querySelector(".topRight").innerHTML = visiblePct;
        box.querySelector(".bottomLeft").innerHTML = visiblePct;
        box.querySelector(".bottomRight").innerHTML = visiblePct;
      });
    }

    function updateAdTimer() {
      if(!po_is_visible) return;
      let lastStarted = po_view_start;
      let currentTime = performance.now();
      if (lastStarted) {
        let diff = currentTime - lastStarted;
        po_total_view = parseFloat(po_total_view) + diff;
      }
      po_view_start = currentTime;
    }

    document.addEventListener(
      "visibilitychange",
      () => {
        if (document.hidden) {
          po_is_visible = 0;
          po_view_start = 0;
          // videoElement.pause();
        } else {
          po_is_visible = 1;
          // videoElement.play();
        }
      },
      false
    );
    // videoElement.addEventListener("pause", function(){
    //   document.title = 'Paused';
    // }, false);
    // // When the video plays, set the title.
    // videoElement.addEventListener("play", function(){
    //   document.title = 'Playing';
    // }, false);
  

MutationObserver



Click, type or resize

    var mo_tests = document.getElementById("mo_tests");

    const MutationObserver = window.MutationObserver ||
                            window.WebKitMutationObserver ||
                            window.MozMutationObserver ||
                            null;
    if (!MutationObserver) {
      battery_tests.innerHTML='MutationObserver not supported';
    }

    // target
    const mo_test_object = document.querySelector("#mo_test_object");
    // options
    const mo_config = {
        attributes: true,
        childList: true,
        characterData: true,
        subtree: true,
    };
    // instance
    const mo_observer = new MutationObserver(function(mutations) {
      mo_tests.innerHTML = `mutations =` + mutations; // MutationRecord
      mutations.forEach(function(mutation) {
        // console.log("mutation =", mutation);
        if (mutation.type === "characterData") {
          // target & object === typeof(mutation.target)
          // console.log(
          //   "A child node has been added OR removed.",
          //   mutation.target,
          //   typeof(mutation.target)
          // );
          // console.log("[...mutation.addedNodes].length", [...mutation.addedNodes].length);
          // console.log("[...mutation.removedNodes].length", [...mutation.removedNodes].length);
          // if (mutation.target && [...mutation.addedNodes].length) {
          //     // [...mutation.addedNodes].length
          //     console.log(`A child node ${mutation.target} has been added!`, mutation.target);
          // }
          // if (mutation.target && [...mutation.removedNodes].length) {
          //     // [...mutation.removedNodes].length
          //     console.log(`A child node ${mutation.target} has been removed!`, mutation.target);
          // }
        }
        if (mutation.type === "childList") {
          if (mutation.target && [...mutation.addedNodes].length) {
            mo_tests.innerHTML += `A child node ${mutation.target} has been added!` + mutation.target;
          }
          if (mutation.target && [...mutation.removedNodes].length) {
            mo_tests.innerHTML += `A child node ${mutation.target} has been removed!` + mutation.target;
          }
          // do somwthings
          let list_values = [];
          list_values = [].slice.call(mo_test_object.children).map(function(node) {
            return node.innerHTML;
          }).filter(function(str) {
            if (str === "") {
              return false;
            } else {
              return true;
            }
          });
          mo_tests.innerHTML += list_values;
        }
        if (mutation.type === "attributes") {
          mo_tests.innerHTML += "mutation = " + mutation;
          mo_tests.innerHTML += `The \`${mutation.attributeName}\` attribute was modified.`;
          // console.log("list style =", list.style);
          let { width, height } = mo_test_object.style;
          let style = { width, height };
          mo_tests.innerHTML += "style = " + JSON.stringify(style, null, 4);
        }
      });
    });
    function startMO (){
      mo_observer.observe(mo_test_object, mo_config);
      mo_tests.innerHTML = "mo_observer.observe(mo_test_object, mo_config)";
    }
    function stopMO (){
      mo_observer.disconnect();
      mo_tests.innerHTML = "mo_observer.disconnect()";
    }
    function addRandomChild(){
      var arc = document.createElement("span");
      arc.innerHTML = utils_randomInt();
      mo_test_object.appendChild(arc);
    }

    // var targetNode = document.querySelector("#someElement");
    // var observerOptions = {
    //   childList: true,
    //   attributes: true,
    //   subtree: true, // omit or set to false to observe only changes to the parent node
    //   attributeOldValue: true,
    //   // watch for changes to the status and username attributes
    //   // in any elements contained within a subtree
    //   attributeFilter: ["status","username"]
    //   // characterDataOldValue: true
    //   // characterData: true
    // }
    // var observer = new MutationObserver(callback);
    // observer.observe(targetNode, observerOptions);

    // // handle any still-pending mutations
    // var mutations = observer.takeRecords();
    // if (mutations) {
    //   callback(mutations);
    // }

    // // observer.disconnect();

    // function callback(mutationList, observer) {
    //   mutationList.forEach((mutation) => {
    //     switch(mutation.type) {
    //       case 'childList':
    //         /* One or more children have been added to and/or removed
    //           from the tree; see mutation.addedNodes and
    //           mutation.removedNodes */
    //         break;
    //       case 'attributes':

    //         "attributeName" + mutation.attributeName +
    //         " changed to " + mutation.target[attributeName] +
    //         " (was " + mutation.oldValue + ")"

    //         switch(mutation.attributeName) {
    //           case "status":
    //             userStatusChanged(mutation.target.username, mutation.target.status);
    //           break;
    //           case "username":
    //             usernameChanged(mutation.oldValue, mutation.target.username);
    //           break;
    //         }

    //         break;
    //     }
    //   });
  

ResizeObserver

So what happened?

And remember, don't do anything that affects anything, unless it turns out you were supposed to, in which case, for the love of God, don't not do it! Ow, my spirit! I don't want to be rescued. You guys aren't Santa! You're not even robots. I've got to find a way to escape the horrible ravages of youth. Suddenly, I'm going to the bathroom like clockwork, every three hours. And those jerks at Social Security stopped sending me checks. Now 'I' have to pay 'them'!


    // change the font-size of a header and paragraph as a slider value is changed
    // causing the containing div to change width
    // shows that you can respond to changes in an element size,
    // even if they have nothing to do with the viewport.
    // checkbox turns the observer off and on: change/not in response to the div width changing

    if(window.ResizeObserver) {
      const h1Elem = document.querySelector('#ro-h1');
      const pElem = document.querySelector('#ro-p');
      const divElem = document.querySelector('#ro-div');
      const slider = document.querySelector('#ro-chk');
      const checkbox = document.querySelector('#ro-rng');

      divElem.style.width = '600px';

      slider.addEventListener('input', () => {
        divElem.style.width = slider.value + 'px';
      })

      const resizeObserver = new ResizeObserver(entries => {
        for (let entry of entries) {
          if(entry.contentBoxSize) {
            // Checking for chrome as using a non-standard array
            if (entry.contentBoxSize[0]) {
              h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize[0].inlineSize/200) + 'rem';
              pElem.style.fontSize = Math.max(1, entry.contentBoxSize[0].inlineSize/600) + 'rem';
            } else {
              h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
              pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
            }

            // canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;
            // canvas.height = entry.devicePixelContentBoxSize[0].blockSize;

          } else {
            h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
            pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
          }
        }
        console.log('Size changed');
      });

      resizeObserver.observe(divElem);

      checkbox.addEventListener('change', () => {
        if(checkbox.checked) {
          resizeObserver.observe(divElem);
        } else {
          resizeObserver.unobserve(divElem);
        }
      });
    } else {
      console.log('Resize observer not supported!');
    }
  

Orientation/Motion

Battery


    var battery_tests = document.getElementById("battery_tests");

    if ('getBattery' in navigator) {
      navigator.getBattery().then(monitorBattery);
    } else {
      battery_tests.innerHTML='Battery API not supported';
    }

    function monitorBattery(battery) {
      batteryInfo(battery);
      battery.addEventListener('levelchange', batteryInfo.bind(null, battery));
      battery.addEventListener('chargingchange', batteryInfo.bind(null, battery));
      battery.addEventListener('dischargingtimechange', batteryInfo.bind(null, battery));
      battery.addEventListener('chargingtimechange', batteryInfo.bind(null, battery));
    }
    function batteryInfo(battery) {
      battery_tests.innerHTML+="battery.level: "+(battery.level*100)+"%";
      battery_tests.innerHTML+="battery.charging: "+(battery.charging?"Yes":"No");
      battery_tests.innerHTML+="battery.chargingTime: "+battery.chargingTime;
      battery_tests.innerHTML+="battery.dischargingTime: "+battery.dischargingTime;
      battery_tests.innerHTML+="----------------------";
    }
  

Bluetooth





All devices:
--------------------------------------------


--------------------------------------------


--------------------------------------------



--------------------------------------------




    var bt_tests = document.getElementById("bt_tests");
    // bt_characteristics, bt_start_notify_characteristics, bt_get_characteristics
    var bt_characteristics = document.getElementById("bt_characteristics");
    // deviceInfo, bt_characteristics, bt_start_notify_characteristics,
    // bt_scan/bt_disconnect/bt_reconnect, bt_get_characteristics
    var bt_services = document.getElementById("bt_services");
    // deviceInfo, bt_scan/bt_disconnect/bt_reconnect
    var bt_device_name = document.getElementById("bt_device_name");
    // deviceInfo, bt_scan/bt_disconnect/bt_reconnect
    var bt_device_name_prefix = document.getElementById("bt_device_name_prefix");
    // deviceInfo
    var bt_all_devices = document.getElementById("bt_all_devices");

    // -----js/bt.js
    // bt_valueToDeviceType
    // bt_valueToUsbVendorName

    // try {
    // const device = await navigator.bluetooth.requestDevice(options);

    if ('bluetooth' in navigator) {
      // navigator.bluetooth.requestDevice(options);
    } else {
      bt_tests.innerHTML='Bluetooth API not supported';
    }

    function bt_device_info() {
      let filters = [];
      let filterService = document.querySelector('#bt_services').value;
      if (filterService.startsWith('0x')) {
        filterService = parseInt(filterService);
      }
      if (filterService) {
        filters.push({services: [filterService]});
      }
      let filterName = document.querySelector('#bt_device_name').value;
      if (filterName) {
        filters.push({name: filterName});
      }
      let filterNamePrefix = document.querySelector('#bt_device_name_prefix').value;
      if (filterNamePrefix) {
        filters.push({namePrefix: filterNamePrefix});
      }
      let options = {};
      if (document.querySelector('#bt_all_devices').checked) {
        options.acceptAllDevices = true;
      } else {
        options.filters = filters;
      }
      bt_tests.innerHTML += "Requesting Bluetooth Device...";
      bt_tests.innerHTML += 'with ' + JSON.stringify(options);
      navigator.bluetooth.requestDevice(options)
      .then(device => {
        bt_tests.innerHTML += '> Name: ' + device.name;
        bt_tests.innerHTML += '> Id: ' + device.id;
        bt_tests.innerHTML += '> Connected: ' + device.gatt.connected;
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }

    function bt_battery_level() {
      bt_tests.innerHTML = "Requesting Bluetooth Device...";
      navigator.bluetooth.requestDevice(
        {filters: [{services: ['battery_service']}]})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting Battery Service...";
        return server.getPrimaryService('battery_service');
      })
      .then(service => {
        bt_tests.innerHTML += "Getting Battery Level Characteristic...";
        return service.getCharacteristic('battery_level');
      })
      .then(characteristic => {
        bt_tests.innerHTML += "Reading Battery Level...";
        return characteristic.readValue();
      })
      .then(value => {
        let batteryLevel = value.getUint8(0);
        bt_tests.innerHTML += '> Battery Level is ' + batteryLevel + "%";
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }

    function bt_characteristics() {
      let serviceUuid = document.querySelector('#bt_services').value;
      if (serviceUuid.startsWith('0x')) {
        serviceUuid = parseInt(serviceUuid);
      }
      let characteristicUuid = document.querySelector('#bt_characteristics').value;
      if (characteristicUuid.startsWith('0x')) {
        characteristicUuid = parseInt(characteristicUuid);
      }
      bt_tests.innerHTML = "Requesting Bluetooth Device...";
      navigator.bluetooth.requestDevice({filters: [{services: [serviceUuid]}]})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting Service...";
        return server.getPrimaryService(serviceUuid);
      })
      .then(service => {
        bt_tests.innerHTML += "Getting Characteristic...";
        return service.getCharacteristic(characteristicUuid);
      })
      .then(characteristic => {
        bt_tests.innerHTML += '> Characteristic UUID: ' +
          characteristic.uuid;
        bt_tests.innerHTML += '> Broadcast: ' +
          characteristic.properties.broadcast;
        bt_tests.innerHTML += '> Read: ' +
          characteristic.properties.read;
        bt_tests.innerHTML += '> Write w/o response: ' +
          characteristic.properties.writeWithoutResponse;
        bt_tests.innerHTML += '> Write: ' +
          characteristic.properties.write;
        bt_tests.innerHTML += '> Notify: ' +
          characteristic.properties.notify;
        bt_tests.innerHTML += '> Indicate: ' +
          characteristic.properties.indicate;
        bt_tests.innerHTML += '> Signed Write: ' +
          characteristic.properties.authenticatedSignedWrites;
        bt_tests.innerHTML += '> Queued Write: ' +
          characteristic.properties.reliableWrite;
        bt_tests.innerHTML += '> Writable Auxiliaries: ' +
          characteristic.properties.writableAuxiliaries;
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }

    var bt_myCharacteristic;
    function bt_start_notify_characteristics() {
      let serviceUuid = document.querySelector('#service').value;
      if (serviceUuid.startsWith('0x')) {
        serviceUuid = parseInt(serviceUuid);
      }
      let characteristicUuid = document.querySelector('#characteristic').value;
      if (characteristicUuid.startsWith('0x')) {
        characteristicUuid = parseInt(characteristicUuid);
      }
      bt_tests.innerHTML = "Requesting Bluetooth Device...";
      navigator.bluetooth.requestDevice({filters: [{services: [serviceUuid]}]})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting Service...";
        return server.getPrimaryService(serviceUuid);
      })
      .then(service => {
        bt_tests.innerHTML += "Getting Characteristic...";
        return service.getCharacteristic(characteristicUuid);
      })
      .then(characteristic => {
        bt_myCharacteristic = characteristic;
        return bt_myCharacteristic.startNotifications().then(_ => {
          bt_tests.innerHTML += "> Notifications started";
          bt_myCharacteristic.addEventListener(
            'characteristicvaluechanged',
            bt_handleNotifications);
        });
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }
    function bt_stop_notify_characteristics() {
      if (bt_myCharacteristic) {
        bt_myCharacteristic.stopNotifications()
        .then(_ => {
          bt_tests.innerHTML += "> Notifications stopped";
          bt_myCharacteristic.removeEventListener(
            'characteristicvaluechanged',
            bt_handleNotifications);
        })
        .catch(error => {
          bt_tests.innerHTML += 'Argh! ' + error;
        });
      }
    }
    function bt_handleNotifications(event) {
      let value = event.target.value;
      let a = [];
      // Convert raw data bytes to hex values just for the sake of showing something.
      // In the "real" world, you'd use data.getUint8, data.getUint16 or even
      // TextDecoder to process raw data bytes.
      for (let i = 0; i < value.byteLength; i++) {
        a.push('0x' + ('00' + value.getUint8(i).toString(16)).slice(-2));
      }
      bt_tests.innerHTML += '> ' + a.join(' ');
    }

    var bluetoothDevice;
    function bt_scan() {
      let options = {filters: []};
      let filterService = document.querySelector('#bt_services').value;
      if (filterService.startsWith('0x')) {
        filterService = parseInt(filterService);
      }
      if (filterService) {
        options.filters.push({services: [filterService]});
      }
      let filterName = document.querySelector('#bt_device_name').value;
      if (filterName) {
        options.filters.push({name: filterName});
      }
      let filterNamePrefix = document.querySelector('#bt_device_name_prefix').value;
      if (filterNamePrefix) {
        options.filters.push({namePrefix: filterNamePrefix});
      }
      bluetoothDevice = null;
      bt_tests.innerHTML = "Requesting Bluetooth Device...";
      navigator.bluetooth.requestDevice(options)
      .then(device => {
        bluetoothDevice = device;
        bluetoothDevice.addEventListener(
          'gattserverdisconnected',
          bt_onDisconnected
        );
        return bt_connect();
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }
    function bt_onDisconnected(event) {
      // Object event.target is Bluetooth Device getting disconnected.
      bt_tests.innerHTML += "> Bluetooth Device disconnected";
    }
    function bt_connect() {
      bt_tests.innerHTML += "Connecting to Bluetooth Device...";
      return bluetoothDevice.gatt.connect()
      .then(server => {
        bt_tests.innerHTML += "> Bluetooth Device connected";
      });
    }
    function bt_disconnect() {
      if (!bluetoothDevice) { return; }
      bt_tests.innerHTML += "Disconnecting from Bluetooth Device...";
      if (bluetoothDevice.gatt.connected) {
        bluetoothDevice.gatt.disconnect();
      } else {
        bt_tests.innerHTML += "> Bluetooth Device is already disconnected";
      }
    }
    function bt_reconnect() {
      if (!bluetoothDevice) { return; }
      if (bluetoothDevice.gatt.connected) {
        bt_tests.innerHTML += "> Bluetooth Device is already connected";
        return;
      }
      bt_connect().catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }

    function bt_get_characteristics() {
      let serviceUuid = document.querySelector('#bt_services').value;
      if (serviceUuid.startsWith('0x')) {
        serviceUuid = parseInt(serviceUuid);
      }
      let characteristicUuid = document.querySelector('#bt_characteristics').value;
      if (characteristicUuid.startsWith('0x')) {
        characteristicUuid = parseInt(characteristicUuid);
      }
      bt_tests.innerHTML = "Requesting Bluetooth Device...";
      navigator.bluetooth.requestDevice({filters: [{services: [serviceUuid]}]})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting Service...";
        return server.getPrimaryService(serviceUuid);
      })
      .then(service => {
        bt_tests.innerHTML += "Getting Characteristics...";
        if (characteristicUuid) {
          // Get all characteristics that match this UUID.
          return service.getCharacteristics(characteristicUuid);
        }
        // Get all characteristics.
        return service.getCharacteristics();
      })
      .then(characteristics => {
        bt_tests.innerHTML += '> Characteristics: ' +
          characteristics.map(c => c.uuid).join('\n' + ' '.repeat(19));
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }

    function bt_device_info_characteristics() {
      bt_tests.innerHTML = "Requesting any Bluetooth Device...";
      navigator.bluetooth.requestDevice({
      // filters: [...] <- Prefer filters to save energy & show relevant devices.
          acceptAllDevices: true,
          optionalServices: ['device_information']})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting Device Information Service...";
        return server.getPrimaryService('device_information');
      })
      .then(service => {
        bt_tests.innerHTML += "Getting Device Information Characteristics...";
        return service.getCharacteristics();
      })
      .then(characteristics => {
        let queue = Promise.resolve();
        let decoder = new TextDecoder('utf-8');
        characteristics.forEach(characteristic => {
          switch (characteristic.uuid) {
            case BluetoothUUID.getCharacteristic('manufacturer_name_string'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> Manufacturer Name String: ' + decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('model_number_string'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> Model Number String: ' + decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('hardware_revision_string'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> Hardware Revision String: ' + decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('firmware_revision_string'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> Firmware Revision String: ' + decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('software_revision_string'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> Software Revision String: ' + decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('system_id'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += "> System ID: ";
                bt_tests.innerHTML += '> Manufacturer Identifier: ' +
                    padHex(value.getUint8(4)) + padHex(value.getUint8(3)) +
                    padHex(value.getUint8(2)) + padHex(value.getUint8(1)) +
                    padHex(value.getUint8(0));
                bt_tests.innerHTML += ' > Organizationally Unique Identifier: ' +
                    padHex(value.getUint8(7)) + padHex(value.getUint8(6)) +
                    padHex(value.getUint8(5));
              });
              break;
            case BluetoothUUID.getCharacteristic('ieee_11073-20601_regulatory_certification_data_list'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += '> IEEE 11073-20601 Regulatory Certification Data List: ' +
                    decoder.decode(value);
              });
              break;
            case BluetoothUUID.getCharacteristic('pnp_id'):
              queue = queue.then(_ => characteristic.readValue()).then(value => {
                bt_tests.innerHTML += "> PnP ID:";
                bt_tests.innerHTML += '  > Vendor ID Source: ' +
                    (value.getUint8(0) === 1 ? 'Bluetooth' : 'USB');
                if (value.getUint8(0) === 1) {
                  bt_tests.innerHTML += '  > Vendor ID: ' +
                      (value.getUint8(1) | value.getUint8(2) << 8);
                } else {
                  bt_tests.innerHTML += '  > Vendor ID: ' +
                      getUsbVendorName(value.getUint8(1) | value.getUint8(2) << 8);
                }
                bt_tests.innerHTML += '  > Product ID: ' +
                    (value.getUint8(3) | value.getUint8(4) << 8);
                bt_tests.innerHTML += '  > Product Version: ' +
                    (value.getUint8(5) | value.getUint8(6) << 8);
              });
              break;
            default: bt_tests.innerHTML += '> Unknown Characteristic: ' + characteristic.uuid;
          }
        });
        return queue;
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }
    /* Utils */
    function padHex(value) {
      return ('00' + value.toString(16).toUpperCase()).slice(-2);
    }
    function getUsbVendorName(value) {
      return value +
          (value in bt_valueToUsbVendorName ? ' (' + bt_valueToUsbVendorName[value] + ')' : '');
    }

    function bt_gap_characteristics() {
      bt_tests.innerHTML = "Requesting any Bluetooth Device...";
      navigator.bluetooth.requestDevice({
      // filters: [...] <- Prefer filters to save energy & show relevant devices.
          acceptAllDevices: true,
          optionalServices: ['generic_access']})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        bt_tests.innerHTML += "Getting GAP Service...";
        return server.getPrimaryService('generic_access');
      })
      .then(service => {
        bt_tests.innerHTML += "Getting GAP Characteristics...";
        return service.getCharacteristics();
      })
      .then(characteristics => {
        let queue = Promise.resolve();
        characteristics.forEach(characteristic => {
          switch (characteristic.uuid) {
            case BluetoothUUID.getCharacteristic('gap.appearance'):
              queue = queue.then(_ => readAppearanceValue(characteristic));
              break;
            case BluetoothUUID.getCharacteristic('gap.device_name'):
              queue = queue.then(_ => readDeviceNameValue(characteristic));
              break;
            case BluetoothUUID.getCharacteristic('gap.peripheral_preferred_connection_parameters'):
              queue = queue.then(_ => readPPCPValue(characteristic));
              break;
            case BluetoothUUID.getCharacteristic('gap.central_address_resolution_support'):
              queue = queue.then(_ => readCentralAddressResolutionSupportValue(characteristic));
              break;
            case BluetoothUUID.getCharacteristic('gap.peripheral_privacy_flag'):
              queue = queue.then(_ => readPeripheralPrivacyFlagValue(characteristic));
              break;
            default: bt_tests.innerHTML += '> Unknown Characteristic: ' + characteristic.uuid;
          }
        });
        return queue;
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }
    function readAppearanceValue(characteristic) {
      return characteristic.readValue().then(value => {
        bt_tests.innerHTML += '> Appearance: ' +
            getDeviceType(value.getUint16(0, true /* Little Endian */));
      });
    }
    function readDeviceNameValue(characteristic) {
      return characteristic.readValue().then(value => {
        bt_tests.innerHTML += '> Device Name: ' + new TextDecoder().decode(value);
      });
    }
    function readPPCPValue(characteristic) {
      return characteristic.readValue().then(value => {
        bt_tests.innerHTML += "Peripheral Preferred Connection Parameters:";
        bt_tests.innerHTML += '  > Minimum Connection Interval: ' +
            (value.getUint8(0) | value.getUint8(1) << 8) * 1.25 + 'ms';
        bt_tests.innerHTML += '  > Maximum Connection Interval: ' +
            (value.getUint8(2) | value.getUint8(3) << 8) * 1.25 + 'ms';
        bt_tests.innerHTML += '  > Slave Latency: ' +
            (value.getUint8(4) | value.getUint8(5) << 8) + 'ms';
        bt_tests.innerHTML += '  > Connection Supervision Timeout Multiplier: ' +
            (value.getUint8(6) | value.getUint8(7) << 8);
      });
    }
    function readCentralAddressResolutionSupportValue(characteristic) {
      return characteristic.readValue().then(value => {
        let supported = value.getUint8(0);
        if (supported === 0) {
          bt_tests.innerHTML += "> Central Address Resolution: Not Supported";
        } else if (supported === 1) {
          bt_tests.innerHTML += "> Central Address Resolution: Supported";
        } else {
          bt_tests.innerHTML += "> Central Address Resolution: N/A";
        }
      });
    }
    function readPeripheralPrivacyFlagValue(characteristic) {
      return characteristic.readValue().then(value => {
        let flag = value.getUint8(0);
        if (flag === 1) {
          bt_tests.innerHTML += "> Peripheral Privacy Flag: Enabled";
        } else {
          bt_tests.innerHTML += "> Peripheral Privacy Flag: Disabled";
        }
      });
    }
    /* Utils */
    function getDeviceType(value) {
      return value +
          (value in bt_valueToDeviceType ? ' (' + bt_valueToDeviceType[value] + ')' : '');
    }

    function bt_discover_services_and_characteristics() {
      // Validate services UUID entered by user first.
      let optionalServices = document.querySelector('#bt_services').value
        .split(/, ?/).map(s => s.startsWith('0x') ? parseInt(s) : s)
        .filter(s => s && BluetoothUUID.getService);
      bt_tests.innerHTML = "Requesting any Bluetooth Device...";
      navigator.bluetooth.requestDevice({
      // filters: [...] <- Prefer filters to save energy & show relevant devices.
          acceptAllDevices: true,
          optionalServices: optionalServices})
      .then(device => {
        bt_tests.innerHTML += "Connecting to GATT Server...";
        return device.gatt.connect();
      })
      .then(server => {
        // Note that we could also get all services that match a specific UUID by
        // passing it to getPrimaryServices().
        bt_tests.innerHTML += "Getting Services...";
        return server.getPrimaryServices();
      })
      .then(services => {
        bt_tests.innerHTML += "Getting Characteristics...";
        let queue = Promise.resolve();
        services.forEach(service => {
          queue = queue.then(_ => service.getCharacteristics().then(characteristics => {
            bt_tests.innerHTML += '> Service: ' + service.uuid;
            characteristics.forEach(characteristic => {
              bt_tests.innerHTML += '>> Characteristic: ' + characteristic.uuid + ' ' +
                  getSupportedProperties(characteristic);
            });
          }));
        });
        return queue;
      })
      .catch(error => {
        bt_tests.innerHTML += 'Argh! ' + error;
      });
    }
    /* Utils */
    function getSupportedProperties(characteristic) {
      let supportedProperties = [];
      for (const p in characteristic.properties) {
        if (characteristic.properties[p] === true) {
          supportedProperties.push(p.toUpperCase());
        }
      }
      return '[' + supportedProperties.join(', ') + ']';
    }
  

INTERFACES

GATT

USB



vendorId:

productId:

classCode:

subclassCode:

protocolCode:

serialNumber:


    if ('usb' in navigator) {
      // navigator.usb.getDevices();
    } else {
      usb_tests.innerHTML='USB API not supported';
    }

    var usb_tests = document.getElementById("usb_tests");
    function usb_get_devices() {
      usb_tests.innerHTML = "";
      navigator.usb.getDevices().then(devices => {
        usb_tests.innerHTML += "Total devices: " + devices.length;
        devices.forEach(device => {
          usb_tests.innerHTML += "Product name: " + device.productName + ", serial number " + device.serialNumber;
          device.addEventListener("connect", (e) => {
            usb_tests.innerHTML += "previously paired device is connected";
          });
          device.addEventListener("disconnect", (e) => {
            usb_tests.innerHTML += "paired device is disconnected";
          });
        });
      });
    }
    function usb_request_device() {
      var rd_v = document.getElementById("rd_v").value; // vendorId
      var rd_p = document.getElementById("rd_p").value; // productId
      var rd_cc = document.getElementById("rd_cc").value; // classCode
      var rd_sc = document.getElementById("rd_sc").value; // subclassCode
      var rd_pc = document.getElementById("rd_pc").value; // protocolCode
      var rd_sn = document.getElementById("rd_sn").value; // serialNumber
      let filters = {};
      rd_v?(filters.vendorId=rd_v):'';
      rd_p?(filters.productId=rd_p):'';
      rd_cc?(filters.classCode=rd_cc):'';
      rd_sc?(filters.subclassCode=rd_sc):'';
      rd_pc?(filters.protocolCode=rd_pc):'';
      rd_sn?(filters.serialNumber=rd_sn):'';
      usb_tests.innerHTML = "Request with: " + JSON.stringify(filters);
      navigator.usb.requestDevice({filters: [
        filters
        // ,{vendorId: 0x1209, productId: 0xa850}
      ]})
      .then(usbDevice => {
        usb_tests.innerHTML += "Product name: " + usbDevice.productName;
      })
      .catch(e => {
        usb_tests.innerHTML += "There is no device. " + e;
      });
    }
  

WEB USB + Arduiono


    var device;
    navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
    .then(selectedDevice => {
        device = selectedDevice;
        return device.open(); // Begin a session.
      })
    .then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
    .then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
    .then(() => device.controlTransferOut({
        requestType: 'class',
        recipient: 'interface',
        request: 0x22,
        value: 0x01,
        index: 0x02})) // Ready to receive data
    .then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
    .then(result => {
      let decoder = new TextDecoder();
      console.log('Received: ' + decoder.decode(result.data));
    })
    .catch(error => { console.log(error); });

    // Third-party WebUSB Arduino library
    #include <WebUSB.h>
    WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");
    #define Serial WebUSBSerial
    void setup() {
      Serial.begin(9600);
      while (!Serial) {
        ; // Wait for serial port to connect.
      }
      Serial.write("WebUSB FTW!");
      Serial.flush();
    }
    void loop() {
      // Nothing here for now.
    }
  

Media











    var media_tests = document.getElementById("media_tests");
    var stream_video = document.getElementById("stream_video");
    var tracks, a_track, v_track;
    var front = false;

    navigator.mediaDevices.ondevicechange = function(event) {
      devicesList();
    }
    function devicesList() {
      media_tests.innerHTML = "";
      if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
        media_tests.innerHTML = "enumerateDevices() not supported";
        return;
      }
      navigator.mediaDevices.enumerateDevices()
      .then(function(devices) {
        devices.forEach(function(device) {
          let [kind,type,direction] = device.kind.match(/(\w+)(input|output)/i);
          media_tests.innerHTML +=
            "--------------------------------" +
            "device.type: " + device.type +
            "direction: " + direction +
            "device.kind: " + device.kind +
            "device.label: " + device.label +
            "device.deviceId: " + device.deviceId +
            "device.groupId: " + device.groupId;
        });
      }).catch(function(err) {
        media_tests.innerHTML = err.name + ": " + error.message;
      });
    }

    var constraints = window.constraints = {
      video: {
        width: { min: 640, ideal: 1920 },
        height: { min: 400, ideal: 1080 },
        aspectRatio: { ideal: 1.7777777778 },
        facingMode: front?"user":"environment"
      },
      audio: true
      // video: {
      //   width: 480,
      //   height: 320,
      //   frameRate: 30,
      //   facingMode: "user" // "environment"
      // }
      // audio: {
      //   sampleRate: 44100,
      //   sampleSize: 16,
      //   volume: 0.25,
      //   channelCount: 2
      // }
      // video: {
      //   width: { min: 1024, ideal: 1280, max: 1920 },
      //   height: { min: 776, ideal: 720, max: 1080 }
      // }
      // video: { facingMode: "user" }
      // video: { facingMode: { exact: "environment" } }
    };

    function startVideoStream() {
      navigator.mediaDevices.getUserMedia(constraints).then(stream => {
        media_tests.innerHTML = "stream.id: " + stream.id;
        stream_video.srcObject = stream;
        // stream.onremovetrack = function(event) {
        //   label.innerHTML = "Removed: " + event.track.kind + ": " + event.track.label;
        // };
        // stream.onaddtrack = function(event) {
        //   label.innerHTML = event.track.kind + ": " + event.track.label;
        // };
        tracks = stream.getTracks();
        let audioTracks = stream.getAudioTracks();
        let videoTracks = stream.getVideoTracks();
        if (audioTracks.length) { a_track = audioTracks[0]; }
        if (videoTracks.length) { v_track = videoTracks[0]; }
        // stream_video.onloadedmetadata = function(e) {
        //   stream_video.play();
        // };
      }).then(function(){
        tracks.forEach(function(track) {
          trackCapSett(track);
          // MediaStreamTrack.clone();
        });
      }).catch(err => {
        media_tests.innerHTML = err
      });
    }
    function stopVideoStream() {
      if (v_track) { v_track.stop(); }
      if (a_track) { audioTrack.stop(); }
      v_track = a_track = null;
      videoElement.srcObject = null;
      // let stream = stream_video.srcObject;
      // let tracks = stream.getTracks();
      // tracks.forEach(function(track) {
      //   track.stop();
      // });
      // stream_video.srcObject = null;
    }

    // change constraints on the fly
    function switchCameras() {
      front = !front;
      constraints.video.facingMode = front?"user":"environment";
      v_track.applyConstraints(constraints);

      stopVideoStream();
      startVideoStream();

      // let stream = stream_video.srcObject;
      // let tracks = stream.getTracks();
      // tracks.forEach(function(track) {
      //   let stream_constraints = track.getConstraints();
      //   stream_constraints.facingMode = front?"user":"environment";
      //   track.applyConstraints(stream_constraints);
      // });

      // const video_constraints = {
      //   width: { min: 640, ideal: 1920, max: 1920 },
      //   height: { min: 400, ideal: 1080 },
      //   aspectRatio: 1.777777778,
      //   frameRate: { max: 30 },
      //   facingMode: { exact: "user" }
      //   advanced: [
      //     {width: 1920, height: 1280},
      //     {aspectRatio: 1.333}
      //   ]
      // };
      // const v_track = stream.getVideoTracks()[0];
      // v_track.applyConstraints(video_constraints).then(() => {
      //   imageCapture = new ImageCapture(v_track);
      //   return imageCapture.getPhotoCapabilities();
      // }).catch(e => {
      //   // The constraints could not be satisfied by the available devices.
      // });

      // // Stop stream after 5 seconds
      // setTimeout(() => {
      //   // const tracks = mediaStream.getVideoTracks();
      //   const tracks = mediaStream.getAudioTracks();
      //   tracks[0].stop()
      // }, 5000)

      // stream.getTrackById("primary-audio-track").applyConstraints({ volume: 0.5 });
      // stream.getTrackById("commentary-track").enabled = true;
    }

    function captureImage() {
      media_tests.innerHTML="";

      if(!v_track){media_tests.innerHTML="No track";return;}
      imageCapture = new ImageCapture(v_track);
      imageCapture.takePhoto().then(function(blob) {
        var img = document.createElement("img");
        img.setAttribute("height","100px");
        img.src = URL.createObjectURL(blob);
        media_tests.appendChild(img);
      }).catch(function(error) {
        media_tests.innerHTML = 'takePhoto() error: '+error;
      });

      imageCapture.getPhotoSettings()
      .then(photoSettings => {
        media_tests.innerHTML +=
          "--------------------------------" +
          "photoSettings.fillLightMode: " + photoSettings.fillLightMode +
          "photoSettings.imageHeight: " + photoSettings.imageHeight +
          "photoSettings.imageWidth: " + photoSettings.imageWidth +
          "photoSettings.redEyeReduction: " + photoSettings.redEyeReduction;
          return imageCapture.getPhotoCapabilities();
      }).then(photoCapabilities => {
        // redEyeReduction:never|always|controllable,
        // imageWidth:range,
        // imageHeight:range,
        // fillLightMode:arr_of_auto|off|flash
        media_tests.innerHTML +=
          "--------------------------------" +
          "photoCapabilities.redEyeReduction: " + photoCapabilities.redEyeReduction +
          "photoCapabilities.imageWidth.min: " + photoCapabilities.imageWidth.min +
          "photoCapabilities.imageWidth.max: " + photoCapabilities.imageWidth.max +
          "photoCapabilities.imageWidth.step: " + photoCapabilities.imageWidth.step +
          "photoCapabilities.imageHeight.min: " + photoCapabilities.imageHeight.min +
          "photoCapabilities.imageHeight.max: " + photoCapabilities.imageWidth.max +
          "photoCapabilities.imageHeight.step: " + photoCapabilities.imageHeight.step +
          "photoCapabilities.fillLightMode: " + photoCapabilities.fillLightMode;
          // // Map zoom to a slider element.
          // input.min = capabilities.zoom.min;
          // input.max = capabilities.zoom.max;
          // input.step = capabilities.zoom.step;
          // input.value = settings.zoom;
          // input.oninput = function(event) {
          //   track.applyConstraints({advanced: [ {zoom: event.target.value} ]});
          // }
      }).catch(error => {
        media_tests.innerHTML += (error.name || error)
      });

      // imageCapture.grabFrame().then(function(imageBitmap) {
      //   console.log('Grabbed frame:', imageBitmap);
      //   canvas.width = imageBitmap.width;
      //   canvas.height = imageBitmap.height;
      //   canvas.getContext('2d').drawImage(imageBitmap, 0, 0);
      //   canvas.classList.remove('hidden');
      // }).catch(function(error) {
      //   console.log('grabFrame() error: ', error);
      // });
      // imageCapture.setOptions({
      //   zoom: zoomInput.value
      // });
    }

    function AtrackCapSett() {
      media_tests.innerHTML="";
      trackCapSett(a_track);
    }
    function VtrackCapSett() {
      media_tests.innerHTML="";
      trackCapSett(v_track);
    }

    function trackCapSett(track) {
      if(!track){media_tests.innerHTML="No track to check";return;}

      media_tests.innerHTML+="---getSettings---";
      let getSettings = track.getSettings();
      for (let sett in getSettings) {
        if (getSettings.hasOwnProperty(sett)) {
          media_tests.innerHTML+=sett+": "+getSettings[sett];
        }
      }
      media_tests.innerHTML+="---getCapabilities---";
      let getCapabilities = track.getCapabilities();
      for (let cap in getCapabilities) {
        if (getCapabilities.hasOwnProperty(cap)) {
          media_tests.innerHTML+=cap+": "+getSettings[cap];
        }
      }
      // track.getSettings().facingMode
      // JSON.stringify(v_track.getSettings(), null, 2);
      // JSON.stringify(a_track.getSettings(), null, 2);
    }

    // pauseButton.onclick = function(evt) {
    //   var newState = !myAudioTrack.enabled;
    //   pauseButton.innerHTML = newState ? "▶️" : "⏸️";
    //   myAudioTrack.enabled = newState;
    // }

    // // Find the canvas element to capture
    // var canvasElt = document.getElementsByTagName("canvas")[0];
    // // Get the stream
    // var stream = canvasElt.captureStream(25); // 25 FPS
    // // Do things to the stream ...
    // // Obtain the canvas associated with the stream
    // var canvas = stream.canvas;

    function getSupportedConstraints() {
      media_tests.innerHTML="";
      let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
      for (let constraint in supportedConstraints) {
        if (supportedConstraints.hasOwnProperty(constraint)) {
          media_tests.innerHTML+=constraint;
        }
      }
    }
    // let supported = navigator.mediaDevices.getSupportedConstraints();
    // document.getElementById("frameRateSlider").disabled = !supported["frameRate"];

    // CAPTURE
    const capture_video = document.getElementById("capture_video");
    var capture_options = {
      video: {cursor:"never"},
      audio: false
    };
    // Set event listeners for the start and stop buttons
    document.getElementById("capture_start").addEventListener("click", function(evt) {
      startCapture();
    }, false);
    document.getElementById("capture_stop").addEventListener("click", function(evt) {
      stopCapture();
    }, false);
    async function startCapture() {
      media_tests.innerHTML="";
      try {
        capture_video.srcObject = await navigator.mediaDevices.getDisplayMedia(capture_options);
        dumpOptionsInfo();
      } catch(err) {
        media_tests.innerHTML="Error: "+err;
      }
    }
    function stopCapture(evt) {
      let tracks = capture_video.srcObject.getTracks();
      tracks.forEach(track => track.stop());
      capture_video.srcObject = null;
    }
    function dumpOptionsInfo() {
      const videoTrack = capture_video.srcObject.getVideoTracks()[0];
      media_tests.innerHTML+="Track settings:";
      media_tests.innerHTML+=JSON.stringify(videoTrack.getSettings(), null, 2);
      media_tests.innerHTML+="Track constraints:";
      media_tests.innerHTML+=JSON.stringify(videoTrack.getConstraints(), null, 2);
    }
  

Animations


    // var animations_tests = document.getElementById("animations_tests");

    // var easingFunctions = [
    //     'frames(10)',
    //     'steps(10)',
    //     'ease-in'
    //   ]
    //   var keyFrames = [
    //     { width: '0%', background : 'red'},
    //     { width: '100%', background : 'blue'},
    //   ]
    //   var divs = document.querySelectorAll('.a_easing');
    //   for(var i = 0; i < divs.length; i++) {
    //     divs[i].animate(keyFrames, {
    //       easing : easingFunctions[i],
    //       duration : 5000,
    //       iterations: Infinity
    //     });
    //   }

    var boxRotationTiming = {
      duration: 1000,
      iterations: 1,
      fill: "none"
    };
    var boxRotationKeyframes = [
      { transform: "rotate(0deg)" },
      { transform: "rotate(360deg)" }
    ];
    document.getElementById("animateButton").addEventListener("click", event => {
      document.getElementById("box").animate(
        boxRotationKeyframes,
        boxRotationTiming
      );
    }, false);

    // // slow down all animations on a page
    // document.getAnimations().forEach(
    //   function (animation) {
    //     animation.playbackRate *= .5;
    //   }
    // );

    var pageTimeline = document.timeline;
    var thisMoment = pageTimeline.currentTime;

    // share a single documentTimeline among multiple animations
    // manipulate just that group of animations via their shared timeline
    // start all the cats animating 500 milliseconds into their animations
    var cats = document.querySelectorAll('.sharedTimelineCat');
    cats = Array.prototype.slice.call(cats);
    var sharedTimeline = new DocumentTimeline({ originTime: 500 });
    cats.forEach(function(cat) {
      var catKeyframes = new KeyframeEffect(cat, keyframes, timing);
      var catAnimation = new Animation(catKeyframes, sharedTimeline);
      catAnimation.play();
    });

    animation.ready.then(function() {
      // Do whatever needs to be done when
      // the animation is ready to run
    });

    // keyframes formats
    // 1 - array of objects (keyframes) consisting of properties and values to iterate
    element.animate([
      { // from
        opacity: 0,
        color: "#fff"
      },
      { // to
        opacity: 1,
        color: "#000"
      }
      // with offset
      // { opacity: 1 },
      // { opacity: 0.1, offset: 0.7 },
      // { opacity: 0 }
      // with easing
      // { opacity: 1, easing: 'ease-out' },
      // { opacity: 0.1, easing: 'ease-in' },
      // { opacity: 0 }
    ], 2000);
    // 2 - object containing key-value pairs consisting of the property to animate
    // and an array of values to iterate over
    element.animate({
      opacity: [ 0, 1 ],          // [ from, to ]
      color:   [ "#fff", "#000" ] // [ from, to ]
      //
      // opacity: [ 0, 1 ], // offset: 0, 1
      // backgroundColor: [ "red", "yellow", "green" ], // offset: 0, 0.5, 1
      //
      // opacity: [ 0, 0.9, 1 ],
      // offset: [ 0, 0.8 ], // Shorthand for [ 0, 0.8, 1 ]
      // easing: [ 'ease-in', 'ease-out' ],
    },2000);
    // property names are specified using camel-case (backgroundColor,backgroundPositionX,margin)
    // exceptions are: cssFloat, cssOffset
    // special attributes: easing, offset, composite

    // Loop through each tear
    tears.forEach(function(el) {
      // Animate each tear
      el.animate(
        tearsFalling,
        {
          delay: getRandomMsRange(-1000, 1000), // randomized for each tear
          duration: getRandomMsRange(2000, 6000), // randomized for each tear
          iterations: Infinity,
          easing: "cubic-bezier(0.6, 0.04, 0.98, 0.335)"
        });
    });
    var getRandomMsRange = function(min, max) {
      return Math.random() * (max - min) + min;
    }

    document.getElementById("alice_arm").animate([
      { transform: 'rotate(10deg)' },
      { transform: 'rotate(-40deg)' }
    ], {
      easing: 'steps(2, end)',
      iterations: Infinity,
      direction: 'alternate',
      duration: 600
    });

    // Define the key frames
    var spriteFrames = [
      { transform: 'translateY(0)' },
      { transform: 'translateY(-100%)' }
    ];
    // Get the element that represents Alice and the Red Queen
    var redQueen_alice_sprite = document.getElementById('red-queen_and_alice_sprite');
    // Animate Alice and the Red Queen using steps()
    var redQueen_alice = redQueen_alice_sprite.animate(
    spriteFrames, {
      easing: 'steps(7, end)',
      direction: "reverse",
      duration: 600,
      playbackRate: 1,
      iterations: Infinity
    });

    // waits until all animations running on the element elem have finished
    // then deletes the element from the DOM tree
    Promise.all(
    elem.getAnimations().map(
      function(animation){
        return animation.finished;
      }
    )).then(
      function() {
        return elem.remove();
      }
    );
    animation.oncancel = function() { animation.effect.target.remove(); };

    // interfaceElement.addEventListener("mousedown", function() {
    //   try {
    //     player.finish();
    //   } catch(e if e instanceof InvalidState) {
    //     console.log("finish() called on paused or finished animation.");
    //   } catch(e) {
    //     logMyErrors(e); //pass exception object to error handler
    //   }
    // });

    speedSelector.addEventListener('input', evt => {
      cartoon.updatePlaybackRate(parseFloat(evt.target.value));
      cartoon.ready.then(() => {
        console.log(`Playback rate set to ${cartoon.playbackRate}`);
      });
    });

    // The cake has its own animation:
    var nommingCake = document.getElementById('eat-me_sprite').animate(
    [
      { transform: 'translateY(0)' },
      { transform: 'translateY(-80%)' }
    ], {
      fill: 'forwards',
      easing: 'steps(4, end)',
      duration: aliceChange.effect.timing.duration / 2
    });
    nommingCake.pause(); // dont play immediately

    // This function will play when ever a user clicks or taps
    var growAlice = function() {
      aliceChange.play();
      nommingCake.play();
    }
    // var shrinkAlice = function() {
    //   aliceChange.playbackRate = -1;
    //   aliceChange.play();
    //   // // same as
    //   // aliceChange.reverse();
    //   drinking.play()
    // }
    // When a user holds their mouse down or taps, call growAlice to make all the animations play.
    cake.addEventListener("mousedown", growAlice, false);
    cake.addEventListener("touchstart", growAlice, false);

    var stopPlayingAlice = function() {
      aliceChange.pause();
      nommingCake.pause();
      drinking.pause();
    };
  

Drag and Drop


    var dnd_div1 = document.getElementById("dnd_div1");
    var dnd_div2 = document.getElementById("dnd_div2");
    var dnd_tests = document.getElementById("dnd_tests");

    function allowDrop(ev) {
      ev.preventDefault();
      ev.target.style.border = '3px dotted blue';
    }

    function drag(ev) {
      ev.dataTransfer.setData("text", ev.target.id);
      // var dataList = ev.dataTransfer.items;
      // dataList.add(ev.target.id, "text/plain");
      // // Add some other items to the drag payload
      // dataList.add("... paragraph ...", "text/html");
      // dataList.add("http://www.example.org","text/uri-list");
    }

    function drop(ev, el) {
      ev.preventDefault();
      var data = ev.dataTransfer.getData("text");
      el.appendChild(document.getElementById(data));
      dnd_div1.style.border = '1px solid black';
      dnd_div2.style.border = '1px solid black';

      // var data = event.dataTransfer.items;
      // for (var i = 0; i < data.length; i += 1) {
      //   if (
      //     (data[i].kind == 'string') &&
      //     (data[i].type.match('^text/plain'))
      //   ) {
      //     // This item is the target node
      //     data[i].getAsString(function (s){
      //       ev.target.appendChild(document.getElementById(s));
      //     });
      //   } else if (
      //     (data[i].kind == 'string') &&
      //     (data[i].type.match('^text/html'))
      //   ) {
      //     // Drag data item is HTML
      //     console.log("... Drop: HTML");
      //   } else if (
      //     (data[i].kind == 'file') &&
      //     (data[i].type.match('^image/'))
      //   ) {
      //     // Drag data item is an image file
      //     var f = data[i].getAsFile();
      //     console.log("... Drop: File ");
      //   }
      // }

    }

    // function dragover_handler(ev) {
    //   console.log("dragOver");
    //   ev.preventDefault();
    //   // Set the dropEffect to move
    //   ev.dataTransfer.dropEffect = "move"
    // }

    // function dragend_handler(ev) {
    //   console.log("dragEnd");
    //   var dataList = ev.dataTransfer.items;
    //   for (var i = 0; i < dataList.length; i++) {
    //     dataList.remove(i);
    //   }
    //   dataList.clear(); // Clear any remaining drag data
    // }

    setInterval(function(){
      dnd_tests.innerHTML = "";
      var div1_childs_ids = "DIV 1:";
      var div1_childs = dnd_div1.children;
      for (var i=0, child; child=div1_childs[i]; i++) {
        div1_childs_ids += child.id
      }
      var div2_childs_ids = "DIV 2:";
      var div2_childs = dnd_div2.children;
      for (var i=0, child; child=div2_childs[i]; i++) {
        div2_childs_ids += child.id
      }
      dnd_tests.innerHTML = div1_childs_ids + div2_childs_ids;
    }, 500);

    // var el = document.getElementById('drag');
    // el.addEventListener("touchstart", handleStart, false);
    // el.addEventListener("touchend", handleEnd, false);
    // el.addEventListener("touchcancel", handleCancel, false);
    // el.addEventListener("touchleave", handleEnd, false);
    // el.addEventListener("touchmove", handleMove, false);
    // function handleStart(event) {
    //     // Handle the start of the touch
    // }
  

Pointer Lock


    var pl_tests = document.querySelector('#pl_tests');
    var canvas = document.querySelector('#pl_tests_canvas');
    var ctx = canvas.getContext('2d');
    var x = 50;
    var y = 50;
    const RADIUS =5;

    function pl_canvasDraw() {
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = "#f00";
      ctx.beginPath();
      ctx.arc(x, y, RADIUS, 0, degToRad(360), true);
      ctx.fill();
    }
    pl_canvasDraw();
    // pointer lock object forking for cross browser
    canvas.requestPointerLock = canvas.requestPointerLock ||
                                canvas.mozRequestPointerLock;
    document.exitPointerLock = document.exitPointerLock ||
                              document.mozExitPointerLock;
    canvas.onclick = function() {
      canvas.requestPointerLock();
    };

    document.addEventListener('pointerlockchange', lockChangeAlert, false);
    document.addEventListener('mozpointerlockchange', lockChangeAlert, false);
    function lockChangeAlert() {
      if (
        document.pointerLockElement === canvas ||
        document.mozPointerLockElement === canvas
      ) {
        pl_logger("pointer lock status is now locked");
        document.addEventListener("mousemove", updatePosition, false);
      } else {
        pl_logger("pointer lock status is now unlocked");
        document.removeEventListener("mousemove", updatePosition, false);
      }
    }

    document.addEventListener('pointerlockerror', lockError, false);
    document.addEventListener('mozpointerlockerror', lockError, false);
    function lockError(e) {
        pl_logger("pointer lock failed");
    }

    var animation;
    function updatePosition(e) {
      x += e.movementX;
      y += e.movementY;
      if (x > canvas.width + RADIUS) {
        x = -RADIUS;
      }
      if (y > canvas.height + RADIUS) {
        y = -RADIUS;
      }
      if (x < -RADIUS) {
        x = canvas.width + RADIUS;
      }
      if (y < -RADIUS) {
        y = canvas.height + RADIUS;
      }
      pl_logger("X position: "+x+", Y position: "+y);
      if (!animation) {
        animation = requestAnimationFrame(function() {
          animation = null;
          pl_canvasDraw();
        });
      }
    }

    function degToRad(degrees) {
      var result = Math.PI / 180 * degrees;
      return result;
    }
    function pl_logger(txt) {
      pl_tests.innerHTML = txt + pl_tests.innerHTML;
    }

    // el.addEventListener("touchstart", handleStart, false);
    // el.addEventListener("touchend", handleEnd, false);
    // el.addEventListener("touchcancel", handleCancel, false);
    // el.addEventListener("touchleave", handleEnd, false);
    // el.addEventListener("touchmove", handleMove, false);
  

Server-Sent Events

  
    <?php
      // php/stream.php file
      header('Content-Type: text/event-stream');
      header('Cache-Control: no-cache');
      $time = date('r');
      echo "data: The server time is: {$time}\n\n";
      flush();
    ?>
  

    var sse_tests = document.getElementById("sse_tests");
    if(typeof(EventSource) !== "undefined") {
      var source = new EventSource("php/stream.php");
      // source.onopen = function(event) {
      //   sse_tests.innerHTML += "Connection to the stream is opened";
      // };
      source.onmessage = function(event) {
        sse_tests.innerHTML += event.data;
      };
      // source.onerror = function(event) {
      //   sse_tests.innerHTML += "Stream errors";
      // };
    } else {
      sse_tests.innerHTML = "server-sent events unsupported...";
    }
  

WebRTC [unfinished]


    var pc = new RTCPeerConnection();
    var state = pc.iceGatheringState;
    var state = pc.iceConnectionState;
    var connectionState = pc.connectionState;

    pc.ontrack = function(event) {
      document.getElementById("received_video").srcObject = event.streams[0];
      document.getElementById("hangup-button").disabled = false;
    };
    pc.onsignalingstatechange = function(event) {
      if (pc.signalingState === "have-local-pranswer") {
        // setLocalDescription() has been called with an answer
      }
    };
    pc.onremovestream = function(ev) { alert("onremovestream event detected!"); };
    pc.onpeeridentity = function(ev) { alert("onpeeridentity event detected!"); };
    pc.onidpvalidationerror = function(ev) { alert("onidpvalidationerror event detected!"); };
    pc.onidpassertionerror = function(ev) { alert("onidpassertionerror event detected!"); };
    pc.onidentityresult = function(ev) { alert("onidentityresult event detected!"); };

    pc.onnegotiationneeded = function() {
      pc.createOffer().then(function(offer) {
        return pc.setLocalDescription(offer);
      })
      .then(function() {
          // Send the offer to the remote peer through the signaling server
        });
      })
      .catch(reportError);
    }

    pc.onicegatheringstatechange = function() {
      let label = "Unknown";
      switch(pc.iceGatheringState) {
        case "new":
        case "complete":
          label = "Idle";
          break;
        case "gathering":
          label = "Determining route";
          break;
      }
      document.getElementById("iceStatus").innerHTML = label;
    }

    pc.oniceconnectionstatechange = function(event) {
      if (pc.iceConnectionState === "failed" ||
          pc.iceConnectionState === "disconnected" ||
          pc.iceConnectionState === "closed") {
        // Handle the failure
      }
    };

    pc.onicecandidate = function(event) {
      if (event.candidate) {
        // Send the candidate to the remote peer
      } else {
        // All ICE candidates have been sent
      }
    }

    pc.ondatachannel = function(ev) {
      console.log('Data channel is created!');
      ev.channel.onopen = function() {
        console.log('Data channel is open and ready to be used.');
      };
    };

    pc.onconnectionstatechange = function(event) {
      switch(pc.connectionState) {
        case "connected":
          // The connection has become fully connected
          break;
        case "disconnected":
        case "failed":
          // One or more transports has terminated unexpectedly or in an error
          break;
        case "closed":
          // The connection has been closed
          break;
      }
    }

    var iceServers = pc.defaultIceServers;
    if (iceServers.length === 0) {
      // Deal with the lack of default ICE servers, possibly by using our own defaults
    }

    var channel = pc.createDataChannel("Mydata");
    channel.onopen = function(event) {
      channel.send('sending a message');
    }
    channel.onmessage = function(event) {
      console.log(event.data);
    }
    // Determine the largest message size that can be sent
    var sctp = pc.sctp;
    var maxMessageSize = sctp.maxMessageSize;

    var sd = pc.remoteDescription;
    if (sd) {
      alert("Remote session: type='" +
      sd.type + "'; sdp description='" +
      sd.sdp + "'");
    }
    else {
      alert("No remote session yet.");
    }

    var sd = pc.currentRemoteDescription;
    if (sd) {
      alert("Local session: type='" +
      sd.type + "'; sdp description='" +
      sd.sdp + "'");
    }
    else {
      alert("No local session yet.");
    }

    var ld = pc.localDescription;
    if (ld) {
      alert("Local session: type='" +
      ld.type + "'; sdp description='" +
      ld.sdp + "'");
    }
    else {
      alert("No local session yet.");
    }

    var sd = pc.currentLocalDescription;
    if (sd) {
      alert("Local session: type='" +
      sd.type + "'; sdp description='" +
      sd.sdp + "'");
    }
    else {
      alert("No local session yet.");
    }

    var identity = pc.peerIdentity;
    if (identity) {
      alert("Identity of the peer: idp='" +
      identity.idp + "'; assertion='" +
      identity.name + "'");
    }
    else {
      alert("Identity of the peer has not been verified");
    }

    // The following code might be used to handle an offer from a peer when
    // it isn't known whether it supports trickle ICE.
    pc.setRemoteDescription(remoteOffer)
      .then(_ => pc.createAnswer())
      .then(answer => pc.setLocalDescription(answer))
      .then(_ =>
        if (pc.canTrickleIceCandidates) {
          return pc.localDescription;
        }
        return new Promise(r => {
          pc.addEventListener('icegatheringstatechange', e => {
            if (e.target.iceGatheringState === 'complete') {
              r(pc.localDescription);
            }
          });
        });
      })
      .then(answer => sendAnswerToPeer(answer)) // signaling message
      .catch(e => handleError(e));
    pc.addEventListener('icecandidate', e => {
      if (pc.canTrickleIceCandidates) {
        sendCandidateToPeer(e.candidate); // signaling message
      }
    });
  

Crypto


    var crypto_tests = document.getElementById("crypto_tests");

    function getRandomNumbers() {
      var array = new Uint32Array(10);
      window.crypto.getRandomValues(array);
      crypto_tests.innerHTML = "random numbers:"
      for (var i = 0; i < array.length; i++) {
        crypto_tests.innerHTML += array[i];
      }
    }

    let text = 'An obscure body in the S-K System, your majesty. The inhabitants refer to it as the planet Earth.';
    async function digestMessage(message) {
      let encoder = new TextEncoder();
      let data = encoder.encode(message);
      let digest = await window.crypto.subtle.digest('SHA-256', data);
      console.log(digest.byteLength); // 32 for SHA-256
    }
    digestMessage(text);

    // RSA-OAEP ENCRYPT

    function getMessageEncoding() {
      const messageBox = document.querySelector(".rsa-oaep #message");
      let message = messageBox.value;
      let enc = new TextEncoder();
      return enc.encode(message);
    }
    function encryptMessage(publicKey) {
      let encoded = getMessageEncoding();
      return window.crypto.subtle.encrypt(
        {
          name: "RSA-OAEP"
        },
        publicKey,
        encoded
      );
    }

    // RSA-OAEP DECRYPT

    function decryptMessage(privateKey, ciphertext) {
      return window.crypto.subtle.decrypt(
        {
          name: "RSA-OAEP"
        },
        privateKey,
        ciphertext
      );
    }

    // RSA-PSS - SIGN AND VERIFY

    // Fetch the contents of the "message" textbox, and encode it
    // in a form we can use for sign operation
    function getMessageEncoding() {
      const messageBox = document.querySelector(".rsa-pss #message");
      let message = messageBox.value;
      let enc = new TextEncoder();
      return enc.encode(message);
    }
    let encoded = getMessageEncoding();
    let signature = await window.crypto.subtle.sign(
      {
        name: "RSA-PSS",
        saltLength: 32,
      },
      privateKey,
      encoded
    );

    // Fetch the contents of the "message" textbox, and encode it
    // in a form we can use for sign operation
    function getMessageEncoding() {
      const messageBox = document.querySelector(".rsa-pss #message");
      let message = messageBox.value;
      let enc = new TextEncoder();
      return enc.encode(message);
    }
    // Fetch the encoded message-to-sign and verify it against the stored signature
    // If it checks out, set the "valid" class on the signature
    // Otherwise set the "invalid" class
    async function verifyMessage(publicKey) {
      const signatureValue = document.querySelector(".rsa-pss .signature-value");
      signatureValue.classList.remove("valid", "invalid");
      let encoded = getMessageEncoding();
      let result = await window.crypto.subtle.verify(
        {
          name: "RSA-PSS",
          saltLength: 32,
        },
        publicKey,
        signature,
        encoded
      );
      signatureValue.classList.add(result ? "valid" : "invalid");
    }

    // KEY PAIR GENERATION

    // RSA-OAEP encryption key pair generation
    let keyPair = window.crypto.subtle.generateKey(
      {
        name: "RSA-OAEP",
        modulusLength: 4096,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: "SHA-256",
      },
      true,
      ["encrypt", "decrypt"]
    );

    // Elliptic curve key pair generation
    let keyPair = window.crypto.subtle.generateKey(
      {
        name: "ECDSA",
        namedCurve: "P-384"
      },
      true,
      ["sign", "verify"]
    );

    // HMAC signing key generation
    let key = window.crypto.subtle.generateKey(
      {
        name: "HMAC",
        hash: {name: "SHA-512"}
      },
      true,
      ["sign", "verify"]
    );

    // AES-GCM encryption key generation
    let key = window.crypto.subtle.generateKey(
      {
        name: "AES-GCM",
        length: 256,
      },
      true,
      ["encrypt", "decrypt"]
    );
  

Performance











    var performance_tests = document.getElementById("performance_tests");
    var performance_obj = null;
    var m1, m2, measure_marks; // for now() cases

    // performance.timeOrigin
    // performance.clearResourceTimings()
    // performance.setResourceTimingBufferSize(max_perf_entries)

    if ('serviceWorker' in navigator) {
      var performance_obj = window.performance;
    } else {
      performance_tests.innerHTML="PerformanceAPI not supported"
    }

    function create_mark(name) {
      performance.mark(name);
    }
    function mark_clear(name) {
      performance.clearMarks(name);
    }
    function create_measure(name) {
      performance.measure(
        name,
        'm1',
        'm2'
      );
    }
    function show_measure(name) {
      performance_tests.innerHTML="";
      var measures = performance.getEntriesByName('measure_marks');
      var measure = measures[0];
      if(!measure){performance_tests.innerHTML="not found...";return;}
      performance_tests.innerHTML =
        "measure.duration: " + measure.duration + "ms" +
        "measure.entryType: " + measure.entryType +
        "measure.name: " + measure.name +
        "measure.startTime: " + measure.startTime;
    }
    function measure_clear(name) {
      if (name === undefined) {
        performance.clearMeasures(); return;
      }
      performance.clearMeasures(name);
    }

    function show_performance_json() {
      performance_tests.innerHTML="";
      var pj = performance.toJSON();
      utils_show_obj(pj, performance_tests);
      performance_tests.innerHTML+=JSON.stringify(pj)+" ]";
    }

    function show_entry(obj) {
      var properties = ["name", "entryType", "startTime", "duration"];
      // var methods = ["toJSON"];
      for (var i=0; i < properties.length; i++) {
        // check each property
        var supported = properties[i] in obj;
        if (supported)
          performance_tests.innerHTML+=properties[i] +
          " = " + obj[properties[i]];
        else
          performance_tests.innerHTML+=properties[i] +
          " = Not supported";
      }
      // for (var i=0; i < methods.length; i++) {
      //   // check each method
      //   var supported = typeof obj[methods[i]] == "function";
      //   if (supported) {
      //     var js = obj[methods[i]]();
      //     performance_tests.innerHTML+=methods[i] + "() = " +
      //     JSON.stringify(js);
      //   } else {
      //     performance_tests.innerHTML+=methods[i] + " = Not supported";
      //   }
      // }
    }

    function run_tests() {
      mark_clear();
      measure_clear();
      performance_tests.innerHTML = "tests ...";

      var t0 = performance.now();
      do_work(7654321+utils_randomInt());
      var t1 = performance.now();
      performance_tests.innerHTML+="measurement with now(): "+(t1-t0)+"ms";

      performance.mark("pm1_start");
      do_work(54321+utils_randomInt());
      performance.mark("pm1_end");

      performance.mark("pm2_start");
      do_work(654321+utils_randomInt());
      performance.mark("pm2_end");

      performance.mark("pm3_start");
      do_work(7654321+utils_randomInt());
      performance.mark("pm3_end");

      // // specific entries
      // p = performance.getEntries({name : "Begin", entryType: "mark"});
      // for (var i=0; i < p.length; i++) {
      //   performance_tests.innerHTML += "Begin[" + i + "]";
      // }

      // "mark" entries
      performance_tests.innerHTML += "mark only entries:"
      var p = performance.getEntriesByType("mark");
      for (var i=0; i < p.length; i++) {
        performance_tests.innerHTML+=p[i].name+" [ start:"+p[i].startTime+
        " duration:"+p[i].duration+" ]";
      }

      // // "mark" entries named "Begin"
      // p = performance.getEntriesByName("Begin", "mark");
      // for (var i=0; i < p.length; i++) {
      //   performance_tests.innerHTML += "Mark and Begin entry[" + i + "]: name = "
      //     + p[i].name +
      //     "startTime = " + p[i].startTime +
      //     "duration  = " + p[i].duration;
      // }

      // each entry
      performance_tests.innerHTML += "all entries:"
      var p = performance.getEntries();
      for (var i=0; i < p.length; i++) {
        performance_tests.innerHTML+=p[i].name+" [ start:"+p[i].startTime+
        " duration:"+p[i].duration+" ]";
      }
    }
    function do_work(cicles) {
      for (let index = 0; index < cicles; index++) {
        var tests = (index+index)+cicles+(utils_randomInt()*cicles+cicles);
        tests*tests;
      }
    }

    function show_navigation() {
      performance_tests.innerHTML=""
      // Use getEntriesByType() to just get the "navigation" events
      var perfEntries = performance.getEntriesByType("navigation");
      for (var i=0; i < perfEntries.length; i++) {
        var p = perfEntries[i];
        performance_tests.innerHTML+="navigation entry [" + i + "]";

        // dom Properties
        performance_tests.innerHTML+="DOM content loaded = " +
          (p.domContentLoadedEventEnd - p.domContentLoadedEventStart);
        performance_tests.innerHTML+="DOM complete = " +
          p.domComplete;
        performance_tests.innerHTML+="DOM interactive = " +
          p.interactive;

        // document load and unload time
        performance_tests.innerHTML+="document load = " +
          (p.loadEventEnd - p.loadEventStart);
        performance_tests.innerHTML+="document unload = " +
          (p.unloadEventEnd - p.unloadEventStart);

        // other properties
        performance_tests.innerHTML+="type = " +
          p.type;
        performance_tests.innerHTML+="redirectCount = " +
          p.redirectCount;
      }
    }

    // interupt current flow !!!
    // var observer = new PerformanceObserver((list, obj) => {
    //   performance_tests.innerHTML="marks created";
    //   var entries = list.getEntries();
    //   for (var i=0; i < entries.length; i++) {
    //     show_entry(entries[i],performance_tests);
    //   }
    //   // observer.disconnect(); // interupt observing
    // });
    // observer.observe({entryTypes: ["mark"]});
    // // var records = observer.takeRecords();
    // // console.log(records[0].name);
    // // console.log(records[0].startTime);
    // // console.log(records[0].duration);
    // var observer2 = new PerformanceObserver((list, observer) => {
    //   performance_tests.innerHTML="measure created";
    //   var entries = list.getEntries();
    //   for (var i=0; i < entries.length; i++) {
    //     show_entry(entries[i],performance_tests);
    //   }
    // });
    // observer2.observe({entryTypes: ["measure"]});

    function show_resources() {
      performance_tests.innerHTML="";
      var p = performance.getEntriesByType("resource");
      for (var i=0; i < p.length; i++) {
        show_entry(p[i]);
        resource_props(p[i]);
      }
    }
    function resource_props(perfEntry) {
      // Print timestamps of the *start and *end properties
      properties = [
        "requestStart",
        "domainLookupStart",
        "domainLookupEnd",
        "connectStart",
        "responseStart",
        "responseEnd",
        "fetchStart",
        "connectEnd",
        "transferSize",
        "encodedBodySize",
        "decodedBodySize",
        "redirectStart",
        "redirectEnd",
        "secureConnectionStart",
        "serverTiming",
        "workerStart",
        "nextHopProtocol",
        "initiatorType"
      ];
      for (var i=0; i < properties.length; i++) {
        // check each property
        var supported = properties[i] in perfEntry;
        if (supported) {
          var value = perfEntry[properties[i]];
          performance_tests.innerHTML+=properties[i] + " = " + value;
        } else {
          performance_tests.innerHTML+=properties[i] + " = NOT supported";
        }
      }
    }