Skip to content

Commit

Permalink
OWN-206 Habr: SW control
Browse files Browse the repository at this point in the history
  • Loading branch information
flancer64 committed Oct 7, 2021
1 parent 7cf6111 commit 5043ec9
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 9 deletions.
1 change: 1 addition & 0 deletions i18n/front/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"home": {
"btnReplace": "Replace",
"btnRestore": "Restore",
"loadingTitle": "URL for loading image:",
"replaceCat": "Cat",
"replaceDog": "Dog",
"title": "Select image to replace to:"
Expand Down
1 change: 1 addition & 0 deletions i18n/front/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"home": {
"btnReplace": "Заместить",
"btnRestore": "Восстановить",
"loadingTitle": "URL загружаемой картинки:",
"replaceCat": "Кот",
"replaceDog": "Пёс",
"title": "Выберите картинку для замещения:"
Expand Down
89 changes: 89 additions & 0 deletions src/Front/Model/Sw/Control.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Service Worker functionality controller.
* Use this object on the front to communicate with Service Worker.
*/
export default class Sw_Control_Front_Model_Sw_Control {
constructor(spec) {
// EXTRACT DEPS
/** @type {typeof Sw_Control_Front_Model_Sw_Message_Type} */
const MSG = spec['Sw_Control_Front_Model_Sw_Message_Type$'];
/** @type {typeof Sw_Control_Front_Model_Sw_Message_Dto} */
const Dto = spec['Sw_Control_Front_Model_Sw_Message_Dto#'];

// DEFINE WORKING VARS / PROPS
/**
* Registry for outgoing messages been sent to SW and theirs callbacks.
* @type {Object<string, function>}
* @private
*/
const _queue = {};

// DEFINE INNER FUNCTIONS
const generateMsgId = () => `${(new Date()).getTime()}`;

/**
* Return SW response (payload) to consumer using callback function from queue.
* @param {MessageEvent} event
*/
function onMessage(event) {
/** @type {Message} */
const msg = event.data;
console.log(`[Control]: backward message from SW, type '${msg.type}'.`);
if (typeof _queue[msg.id] === 'function') _queue[msg.id](msg.payload);
}

// DEFINE INSTANCE METHODS
/**
* Get state of the service worker.
* 'true' - 'cat' URL is selected as secondary image, 'false' - 'dog' URL.
* @return {Promise<boolean>}
*/
this.getState = function () {
// generate ID to register data parsing callback
const id = generateMsgId();
// create and return new promise
return new Promise(async (resolve) => {
// create and register callback to process SW backward message.
_queue[id] = function (payload) {
// return 'boolean' result to caller (see SW code)
console.log(`[Control]: payload is returned to caller: ${JSON.stringify(payload)}`);
resolve(payload);
};
// create new DTO to send data to SW
const msg = new Dto();
msg.id = id;
msg.type = MSG.GET_STATE;
// send message to SW
console.log(`[Control]: send message to SW to get current state.`);
const sw = await navigator.serviceWorker.ready;
if (sw.active) sw.active.postMessage(msg);
});
};

this.setState = function (replaceWithCat = true) {
const id = generateMsgId();
return new Promise(async (resolve) => {
// create and register callback to process SW backward message.
_queue[id] = function (payload) {
// return 'boolean' result to caller (see SW code)
console.log(`[Control]: payload is returned to caller: ${JSON.stringify(payload)}.`);
resolve(payload);
};
// create new DTO to send data to SW
const msg = new Dto();
msg.id = id;
msg.type = MSG.SET_STATE;
msg.payload = replaceWithCat;
// send message to SW
const logState = (replaceWithCat) ? 'cat' : 'dog';
console.log(`[Control]: send message to SW to set state: ${logState}.`);
const sw = await navigator.serviceWorker.ready;
sw.active.postMessage(msg);
});
};

// MAIN FUNCTIONALITY
self.navigator.serviceWorker.addEventListener('message', onMessage);
}

}
11 changes: 11 additions & 0 deletions src/Front/Model/Sw/Message/Dto.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Service worker message structure.
*/
export default class Sw_Control_Front_Model_Sw_Message_Dto {
/** @type {string} */
id;
/** @type {*} */
payload;
/** @type {typeof Sw_Control_Front_Model_Sw_Message_Type} */
type;
}
9 changes: 9 additions & 0 deletions src/Front/Model/Sw/Message/Type.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Enumeration for Service Worker messages.
*/
const Sw_Control_Front_Model_Sw_Message_Type = {
GET_STATE: 'get_state',
SET_STATE: 'set_state',
}
Object.freeze(Sw_Control_Front_Model_Sw_Message_Type);
export default Sw_Control_Front_Model_Sw_Message_Type;
57 changes: 48 additions & 9 deletions src/Front/Widget/Home/Route.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// MODULE'S VARS
const NS = 'Sw_Control_Front_Widget_Home_Route';
const IMG_CLASS = 'imageToDisplay';
const IMG_SRC_PRIM = './img/horse.svg';
const IMG_SRC_SEC = './img/cat.svg';
const IMG_SRC_PRIM = './img/primary.svg';
const IMG_SRC_SEC = './img/secondary.svg';
const REPLACE_CAT = 'cat';
const REPLACE_DOG = 'dog';

Expand All @@ -22,6 +22,8 @@ export default function Factory(spec) {
// EXTRACT DEPS
/** @type {Sw_Control_Front_Defaults} */
const DEF = spec['Sw_Control_Front_Defaults$'];
/** @type {Sw_Control_Front_Model_Sw_Control} */
const swControl = spec['Sw_Control_Front_Model_Sw_Control$'];

// DEFINE WORKING VARS
const template = `
Expand All @@ -30,8 +32,16 @@ export default function Factory(spec) {
<q-card class="bg-white q-pa-xs text-center">
<div>{{$t('widget.home.title')}}</div>
<div class="q-gutter-md">
<q-radio v-model="replace" val="${REPLACE_CAT}" :label="$t('widget.home.replaceCat')"/>
<q-radio v-model="replace" val="${REPLACE_DOG}" :label="$t('widget.home.replaceDog')"/>
<q-radio val="${REPLACE_CAT}"
:disable="!isPrimaryImg"
:label="$t('widget.home.replaceCat')"
v-model="replace"
/>
<q-radio v-model="replace"
:disable="!isPrimaryImg"
val="${REPLACE_DOG}"
:label="$t('widget.home.replaceDog')"
/>
<q-btn
@click="reload"
color="primary"
Expand All @@ -41,7 +51,11 @@ export default function Factory(spec) {
</q-card>
<q-card class="bg-white q-pa-xs">
<q-img class="${IMG_CLASS}" :src="url" class="q-mt-md" width="50%" />
<q-img class="${IMG_CLASS}" :src="url" class="q-mt-md" height="200px" fit="scale-down"/>
</q-card>
<q-card class="bg-white q-pa-xs">
<div class="text-center">{{$t('widget.home.loadingTitle')}}</div>
<pre>{{printLoaded}}</pre>
</q-card>
</div>
</layout-base>
Expand All @@ -60,21 +74,46 @@ export default function Factory(spec) {
template,
data: function () {
return {
replace: REPLACE_CAT,
loaded: [IMG_SRC_PRIM],
replace: null,
url: IMG_SRC_PRIM,
};
},
computed: {
btnLabel() {
return (this.url === IMG_SRC_PRIM)
return (this.isPrimaryImg)
? this.$t('widget.home.btnReplace')
: this.$t('widget.home.btnRestore');
},
isPrimaryImg() {
return (this.url === IMG_SRC_PRIM);
},
printLoaded() {
return this.loaded.join('\n');
},
useCat() {
return (this.replace === REPLACE_CAT);
}
},
methods: {
reload() {
async reload() {
this.url = (this.url === IMG_SRC_PRIM) ? IMG_SRC_SEC : IMG_SRC_PRIM;
this.loaded.unshift(this.url);
}
},
watch: {
async replace(now, old) {
if ((old !== null) && (old !== now)) {
console.log(`[App]: set SW state to: ${now}.`);
const res = await swControl.setState(now === REPLACE_CAT);
console.log(`[App]: service worker state modification result: ${res}.`);
}
}
}
},
async mounted() {
const res = await swControl.getState();
this.replace = (res) ? REPLACE_CAT : REPLACE_DOG;
console.log(`[App]: current SW state: ${this.replace}.`);
},
};
}
File renamed without changes
92 changes: 92 additions & 0 deletions web/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// DEFINE WORKING VARS / PROPS
const CACHE_STATIC = 'sw-static-cache-v1';
const URL_CAT = '/img/cat.svg';
const URL_DOG = '/img/dog.svg';
const URL_PRIMARY = '/img/primary.svg';
const URL_SECONDARY = '/img/secondary.svg'; // this is virtual URL that is mapped to real URL (dog or cat)

let _useCat = true; // SW state to replace virtual URL

// DEFINE INNER FUNCTIONS

/**
* Send message to `index.html` to start bootstrap process after installation process been completed.
*/
function onActivate() {
self.clients.claim();
}

/**
* Replace secondary URL with 'cat' or 'dog' URL.
* @param event
*/
function onFetch(event) {
const url = new URL(event.request.url);
if (url.origin === location.origin && url.pathname === URL_SECONDARY) {
if (_useCat) {
event.respondWith(caches.match(URL_CAT));
} else {
event.respondWith(caches.match(URL_DOG));
}

}
}

/**
* Load images to the cache storage.
* @param event
*/
function onInstall(event) {
// DEFINE INNER FUNCTIONS
async function cacheStatics() {
try {
const files = [URL_CAT, URL_DOG, URL_PRIMARY];
const cacheStat = await caches.open(CACHE_STATIC);
await cacheStat.addAll(files);
} catch (e) {
console.log('[SW] install error: ');
console.dir(e);
}
}

// MAIN FUNCTIONALITY
event.waitUntil(cacheStatics());
}

/**
* Get messages from app and change internal state.
* @param {MessageEvent} event
*/
function onMessage(event) {
// get incoming data
/** @type {Sw_Control_Front_Model_Sw_Message_Dto} */
const msg = event.data;
console.log(`[SW]: new message is received, type: ${msg.type}`);
// create outgoing data
/** @type {Sw_Control_Front_Model_Sw_Message_Dto} */
const res = {};
res.id = msg.id;
res.type = msg.type;
// analyze type of the message, perform requested operation and set outgoing data
if (msg.type === 'get_state') {
res.payload = _useCat;
} else if (msg.type === 'set_state') {
const useCat = msg.payload;
if (useCat !== _useCat) {
// change state
_useCat = useCat;
res.payload = true;
} else {
// we don't change anything
res.payload = false;
}
}
console.log(`[SW]: send backward message, type: ${msg.type}`);
event.source.postMessage(res);
}

// MAIN FUNCTIONALITY
self.addEventListener('activate', onActivate);
self.addEventListener('fetch', onFetch);
self.addEventListener('install', onInstall);
self.addEventListener('message', onMessage);

0 comments on commit 5043ec9

Please sign in to comment.