SlideShare a Scribd company logo
1 of 31
Download to read offline
Reliable, Fast, Engaging Offline-First
Architecture for JavaScript Applications
Andrejus Baranovskis, CEO andTechnical Expert, Red Samurai Consulting
Oracle ACE Director and Oracle Groundbreaker Ambassador
Florin Marcus,Technical Expert, Red Samurai Consulting
Oracle ExpertsTeam
ADF, JET, ORACLE FUSION, ORACLE CLOUD, MACHINE LEARNING
Oracle PaaS Partner Community Award for Outstanding Java Cloud
Service Contribution 2017
AGENDA
• Offline-First Architecture Why and How
• Technical Implementation
• Solution WalkThrough
• Offline PersistenceToolkit Config for JavaScript (Oracle JET)
OFFLINE-FIRST ARCHITECTURE
WHY AND HOW
No Wi-Fi?
No Problem
Build web/mobile applications that can work great,
whatever the connection
TECHNICAL IMPLEMENTATION
Oracle JET
Offline PersistenceToolkit
Online
Log Request for Replay
Local Storage for
Temporary Data
request/response yes
no
KEY POINTS
• Business logic code stays the same, simple for developers, no code
intrusion with offline toolkit API
• API is very rich, many options to override and control
• Configurable and flexible, e.g. store factory can be changed
• API is heavily based on JS Promise - represents the eventual completion
(or failure) of an asynchronous operation, and its resulting value
KEY POINTS
• Offline:
• Request is logged and inserted into replay queue automatically
• If user is changing data, developer must implement request update logic - in custom
handlePatch or handlePost methods
• Online:
• Possible to define before and after synch listeners.This helps to control request data
• Can control request synchronisation queue execution
SOLUTION WALKTHROUGH
ENDPOINT REGISTRATION
Register
Endpoint
Define Shredder and
Query Handler
Provide handlePatch
method
Define
before/after
listeners
ENDPOINT REGISTRATION
persistenceManager.init().then(function() {
persistenceManager.register({scope: '/Employees'})
.then(function(registration) {
var responseProxy = defaultResponseProxy.getResponseProxy({
jsonProcessor: {
shredder: oracleRestJsonShredding.getShredder('emp', 'EmployeeId'),
unshredder: oracleRestJsonShredding.getUnshredder()
},
queryHandler: queryHandlers.getOracleRestQueryHandler('emp'),
requestHandlerOverride: {handlePatch: customHandlePatch}
});
var fetchListener = responseProxy.getFetchEventListener();
registration.addEventListener('fetch', fetchListener);
// initial data load
self.fetchData();
});
// handles request data before synch
persistenceManager.getSyncManager().addEventListener('beforeSyncRequest', self.beforeRequestListener, '/Employees' );
// handles response data after synch
persistenceManager.getSyncManager().addEventListener('syncRequest', self.afterRequestListener, '/Employees' );
});
OFFLINE UPDATE OR CREATION
Offline
yes
Get request
data
Open offline
store
Find data by
key in offline
store
Update data
in offline store
OFFLINE UPDATE OR CREATION
var customHandlePatch = function(request) {
if (!persistenceManager.isOnline()) {
persistenceUtils.requestToJSON(request).then(function (data) {
var requestData = JSON.parse(data.body.text);
persistenceStoreManager.openStore('emp').then(function (store) {
store.findByKey(requestData.EmployeeId).then(function (data) {
data.FirstName = requestData.FirstName;
data.ChangeIndicatorAttr = requestData.ChangeIndicatorAttr;
store.upsert(requestData.EmployeeId, JSON.parse('{}'), data);
})
});
})
var init = {'status': 503, 'statusText': 'Edit will be processed when online'};
return Promise.resolve(new Response(null, init));
} else {
return persistenceManager.browserFetch(request);
}
};
ONLINE REPLAY
Check if request
should be
replayed
Execute replay If replay error occurs,
ask user feedback and
read info from response
Clear up
helper variables
when replay
completes
ONLINE REPLAY
persistenceManager.getSyncManager().sync({preflightOptionsRequest: 'disabled'}).then(function () {
employeeSynchMap = {};
console.log('SYNCH DONE');
}, function (error) {
var statusCode = error.response.status;
if (statusCode == 409) {
// conflict during offline data synch
$("#md3").ojDialog("open");
synchErrorRequestId = error.requestId;
var response = error.response;
response.json().then(function (value) {
self.onlineConflictResolutionTitle('Conflict resolution: ' + value.FirstName);
synchErrorChangeIndicatorAttr = value.ChangeIndicatorAttr;
});
}
}
);
AFTER REQUEST LISTENER
Get response
data
Update local change
indicator value with the
one from response
Save data ID and change
indicator value
AFTER REQUEST LISTENER
self.afterRequestListener = function (event) {
// invoked if offline synch for request was success, to bring back values updated in backend
var statusCode = event.response.status;
if (statusCode == 200) {
event.response.json().then(function(response) {
var id = response.EmployeeId;
var changeIndicatorAttr = response.ChangeIndicatorAttr;
for (var i = 0; i < self.allItems().length; i++) {
if (self.allItems()[i].id === id) {
self.allItems.splice(i, 1, {"id": self.allItems()[i].id, "firstName": self.allItems()[i].firstName,
"lastName": self.allItems()[i].lastName, "email": self.allItems()[i].email,
"phoneNumber": self.allItems()[i].phoneNumber,
"changeIndicatorAttr": changeIndicatorAttr});
employeeSynchMap[id] = changeIndicatorAttr;
console.log('UPDATE SUCCESS IN SYNCH FOR: ' + id + ',WITH NEW CHANGE INDICATOR: ' + changeIndicatorAttr);
break;
}
}
});
}
return Promise.resolve({action: 'continue'});
}
BEFORE REQUEST LISTENER
Construct request
promise
Get request
data
Check if change
indicator value
must be updated
Update request
and continue
BEFORE REQUEST LISTENER
self.beforeRequestListener = function (event) {
var request = event.request;
return new Promise(function (resolve, reject) {
persistenceUtils.requestToJSON(request).then(function (data) {
var empl = JSON.parse(data.body.text);
var employeeId = empl.EmployeeId;
var updateRequest = false;
for (var i in employeeSynchMap) {
if (parseInt(i) === parseInt(employeeId)) {
updateRequest = true;
var changeIndicatorVal = employeeSynchMap[i];
empl.ChangeIndicatorAttr = changeIndicatorVal;
data.body.text = JSON.stringify(empl);
persistenceUtils.requestFromJSON(data).then(function (updatedRequest) {
resolve({action: 'replay', request: updatedRequest});
});
break;
}}
if (!updateRequest) {
resolve({action: 'continue'});
}
CONFLICT - APPLY CLIENT CHANGES
Remove request Read request
data
Update change
indicator value and
construct new request
Insert new request
into queue and call
replay
CONFLICT - APPLY CLIENT CHANGES
self.applyOfflineClientChangesToServer = function(event) {
persistenceManager.getSyncManager().removeRequest(synchErrorRequestId).then(function (request) {
$("#md3").ojDialog("close");
persistenceUtils.requestToJSON(request).then(function (requestData) {
var requestPayload = JSON.parse(requestData.body.text);
requestPayload.ChangeIndicatorAttr = synchErrorChangeIndicatorAttr;
requestData.body.text = JSON.stringify(requestPayload);
persistenceUtils.requestFromJSON(requestData).then(function (request) {
persistenceManager.getSyncManager().insertRequest(request).then(function () {
self.synchOfflineChanges();
});
});
});
});
}
CONFLICT - APPLY SERVER CHANGES
Refresh client side entry
by calling backend
service
Call replay to
continue
Remove request Read request
data
CONFLICT - APPLY SERVER CHANGES
self.cancelOfflineClientChangesToServer = function(event) {
persistenceManager.getSyncManager().removeRequest(synchErrorRequestId).then(function (request) {
$("#md3").ojDialog("close");
persistenceUtils.requestToJSON(request).then(function (requestData) {
var requestPayload = JSON.parse(requestData.body.text);
var employeeId = requestPayload.EmployeeId;
var searchUrl = empls.getEmployeesEndpointURL() + "/" + employeeId;
self.refreshEntry(searchUrl);
});
self.synchOfflineChanges();
});
}
OFFLINE PERSISTENCETOOLKIT CONFIG
FOR JAVASCRIPT (ORACLE JET)
PATH MAPPING: OFFLINETOOLKIT
"offline": {
"cdn": "",
"cwd": "node_modules/@oracle/offline-persistence-toolkit/dist/debug",
"debug": {
"src": ["*.js", "impl/*.js"],
"path": "libs/offline-persistence-toolkit/v1.1.5/dist",
"cdnPath": ""
}
}
PATH MAPPING: POUCH DB
"pouchdb": {
"cdn": "",
"cwd": "node_modules/pouchdb/dist",
"debug": {
"src": ["*.js"],
"path": "libs/pouchdb/v6.3.4/dist/pouchdb.js",
"cdnPath": ""
}
}
require(['pouchdb'], function (pouchdb) {
window.PouchDB = pouchdb;
});
DEFINE BLOCK FOR OFFLINETOOLKIT
define(['ojs/ojcore', 'knockout', 'jquery',
'offline/persistenceStoreManager',
'offline/pouchDBPersistenceStoreFactory',
'offline/persistenceManager',
'offline/defaultResponseProxy',
'offline/oracleRestJsonShredding',
'offline/queryHandlers',
'offline/persistenceUtils',
'offline/impl/logger',
'viewModels/helpers/employeesHelper',
QUESTIONS
CONTACTS
• Andrejus Baranovskis (https://andrejusb.blogspot.com)
• Email: abaranovskis@redsamuraiconsulting.com
• Twitter: @andrejusb
• LinkedIn: https://www.linkedin.com/in/andrejus-baranovskis-251b392
• Web: http://redsamuraiconsulting.com
REFERENCES
• Source Code on GitHub - https://bit.ly/2PU4iaw
• Oracle OfflineToolkit on GitHub - https://bit.ly/2I7Nr11
• Oracle JET - https://bit.ly/2O21GtS

More Related Content

Similar to Reliable, Fast, Engaging Offline-First Architecture for JavaScript Applications

Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019Joe Keeley
 
Introduction to Angular js
Introduction to Angular jsIntroduction to Angular js
Introduction to Angular jsMustafa Gamal
 
Testdrevet javautvikling på objektorienterte skinner
Testdrevet javautvikling på objektorienterte skinnerTestdrevet javautvikling på objektorienterte skinner
Testdrevet javautvikling på objektorienterte skinnerTruls Jørgensen
 
CiklumJavaSat_15112011:Alex Kruk VMForce
CiklumJavaSat_15112011:Alex Kruk VMForceCiklumJavaSat_15112011:Alex Kruk VMForce
CiklumJavaSat_15112011:Alex Kruk VMForceCiklum Ukraine
 
API Days Australia - Automatic Testing of (RESTful) API Documentation
API Days Australia  - Automatic Testing of (RESTful) API DocumentationAPI Days Australia  - Automatic Testing of (RESTful) API Documentation
API Days Australia - Automatic Testing of (RESTful) API DocumentationRouven Weßling
 
Service workers and the role they play in modern day web apps
Service workers and the role they play in modern day web appsService workers and the role they play in modern day web apps
Service workers and the role they play in modern day web appsMukul Jain
 
Introduction to Struts
Introduction to StrutsIntroduction to Struts
Introduction to Strutselliando dias
 
Adding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsAdding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsJeff Durta
 
Asynchronous Interfaces
Asynchronous InterfacesAsynchronous Interfaces
Asynchronous Interfacesmaccman
 
How to separate the f2 e and sde in web development for_taobao
How to separate the f2 e and sde in web development for_taobaoHow to separate the f2 e and sde in web development for_taobao
How to separate the f2 e and sde in web development for_taobaotaobao.com
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorialKaty Slemon
 
Azure Durable Functions (2019-03-30)
Azure Durable Functions (2019-03-30) Azure Durable Functions (2019-03-30)
Azure Durable Functions (2019-03-30) Paco de la Cruz
 
Vertx - Reactive & Distributed
Vertx - Reactive & DistributedVertx - Reactive & Distributed
Vertx - Reactive & DistributedOrkhan Gasimov
 
Velocity EU 2014 — Offline-first web apps
Velocity EU 2014 — Offline-first web appsVelocity EU 2014 — Offline-first web apps
Velocity EU 2014 — Offline-first web appsandrewsmatt
 

Similar to Reliable, Fast, Engaging Offline-First Architecture for JavaScript Applications (20)

Introduction to BreezeJs
Introduction to BreezeJsIntroduction to BreezeJs
Introduction to BreezeJs
 
Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019Rethinking Syncing at AltConf 2019
Rethinking Syncing at AltConf 2019
 
Introduction to Angular js
Introduction to Angular jsIntroduction to Angular js
Introduction to Angular js
 
Testdrevet javautvikling på objektorienterte skinner
Testdrevet javautvikling på objektorienterte skinnerTestdrevet javautvikling på objektorienterte skinner
Testdrevet javautvikling på objektorienterte skinner
 
CiklumJavaSat_15112011:Alex Kruk VMForce
CiklumJavaSat_15112011:Alex Kruk VMForceCiklumJavaSat_15112011:Alex Kruk VMForce
CiklumJavaSat_15112011:Alex Kruk VMForce
 
API Days Australia - Automatic Testing of (RESTful) API Documentation
API Days Australia  - Automatic Testing of (RESTful) API DocumentationAPI Days Australia  - Automatic Testing of (RESTful) API Documentation
API Days Australia - Automatic Testing of (RESTful) API Documentation
 
Ajax Tuturial
Ajax TuturialAjax Tuturial
Ajax Tuturial
 
Service workers and the role they play in modern day web apps
Service workers and the role they play in modern day web appsService workers and the role they play in modern day web apps
Service workers and the role they play in modern day web apps
 
Introduction to Struts
Introduction to StrutsIntroduction to Struts
Introduction to Struts
 
Struts Intro
Struts IntroStruts Intro
Struts Intro
 
Adding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsAdding a modern twist to legacy web applications
Adding a modern twist to legacy web applications
 
Asynchronous Interfaces
Asynchronous InterfacesAsynchronous Interfaces
Asynchronous Interfaces
 
RIA
RIARIA
RIA
 
JavaScript Promise
JavaScript PromiseJavaScript Promise
JavaScript Promise
 
How to separate the f2 e and sde in web development for_taobao
How to separate the f2 e and sde in web development for_taobaoHow to separate the f2 e and sde in web development for_taobao
How to separate the f2 e and sde in web development for_taobao
 
Thinking Beyond ORM in JPA
Thinking Beyond ORM in JPAThinking Beyond ORM in JPA
Thinking Beyond ORM in JPA
 
Angular resolver tutorial
Angular resolver tutorialAngular resolver tutorial
Angular resolver tutorial
 
Azure Durable Functions (2019-03-30)
Azure Durable Functions (2019-03-30) Azure Durable Functions (2019-03-30)
Azure Durable Functions (2019-03-30)
 
Vertx - Reactive & Distributed
Vertx - Reactive & DistributedVertx - Reactive & Distributed
Vertx - Reactive & Distributed
 
Velocity EU 2014 — Offline-first web apps
Velocity EU 2014 — Offline-first web appsVelocity EU 2014 — Offline-first web apps
Velocity EU 2014 — Offline-first web apps
 

More from andrejusb

Machine Learning Applied - Tabular Dataset Models and Sentiment Analysis
Machine Learning Applied - Tabular Dataset Models and Sentiment AnalysisMachine Learning Applied - Tabular Dataset Models and Sentiment Analysis
Machine Learning Applied - Tabular Dataset Models and Sentiment Analysisandrejusb
 
JavaScript Development on Steroids with Oracle Visual Builder
JavaScript Development on Steroids with Oracle Visual BuilderJavaScript Development on Steroids with Oracle Visual Builder
JavaScript Development on Steroids with Oracle Visual Builderandrejusb
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlowMachine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlowandrejusb
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...
 Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor... Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...andrejusb
 
Microservice Approach for Web Development with Micro Frontends
Microservice Approach for Web Development with Micro FrontendsMicroservice Approach for Web Development with Micro Frontends
Microservice Approach for Web Development with Micro Frontendsandrejusb
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorflowMachine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflowandrejusb
 
Oracle Development Cloud Service
Oracle Development Cloud ServiceOracle Development Cloud Service
Oracle Development Cloud Serviceandrejusb
 
Oracle Java Cloud Service: How to Estimate Production System Performance
Oracle Java Cloud Service: How to Estimate Production System PerformanceOracle Java Cloud Service: How to Estimate Production System Performance
Oracle Java Cloud Service: How to Estimate Production System Performanceandrejusb
 
Essential Kit for Oracle JET Programming
Essential Kit for Oracle JET ProgrammingEssential Kit for Oracle JET Programming
Essential Kit for Oracle JET Programmingandrejusb
 
Oracle JET and ADF BC REST Production Experience with Oracle Java Cloud
Oracle JET and ADF BC REST Production Experience with Oracle Java CloudOracle JET and ADF BC REST Production Experience with Oracle Java Cloud
Oracle JET and ADF BC REST Production Experience with Oracle Java Cloudandrejusb
 
Offline Web with Oracle JET
Offline Web with Oracle JETOffline Web with Oracle JET
Offline Web with Oracle JETandrejusb
 
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...andrejusb
 
Forms, ADF and JET a Non-Aggression Pact
Forms, ADF and JET a Non-Aggression PactForms, ADF and JET a Non-Aggression Pact
Forms, ADF and JET a Non-Aggression Pactandrejusb
 
Oracle JET CRUD and ADF BC REST
Oracle JET CRUD and ADF BC RESTOracle JET CRUD and ADF BC REST
Oracle JET CRUD and ADF BC RESTandrejusb
 
Oracle JET and WebSocket
Oracle JET and WebSocketOracle JET and WebSocket
Oracle JET and WebSocketandrejusb
 
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI Support
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI SupportOracle Alta UI Patterns for Enterprise Applications and Responsive UI Support
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI Supportandrejusb
 
ADF Mythbusters UKOUG'14
ADF Mythbusters UKOUG'14ADF Mythbusters UKOUG'14
ADF Mythbusters UKOUG'14andrejusb
 
Data Caching Strategies for Oracle Mobile Application Framework
Data Caching Strategies for Oracle Mobile Application FrameworkData Caching Strategies for Oracle Mobile Application Framework
Data Caching Strategies for Oracle Mobile Application Frameworkandrejusb
 
ADF Development Survival Kit
ADF Development Survival KitADF Development Survival Kit
ADF Development Survival Kitandrejusb
 
ADF Anti-Patterns: Dangerous Tutorials
ADF Anti-Patterns: Dangerous TutorialsADF Anti-Patterns: Dangerous Tutorials
ADF Anti-Patterns: Dangerous Tutorialsandrejusb
 

More from andrejusb (20)

Machine Learning Applied - Tabular Dataset Models and Sentiment Analysis
Machine Learning Applied - Tabular Dataset Models and Sentiment AnalysisMachine Learning Applied - Tabular Dataset Models and Sentiment Analysis
Machine Learning Applied - Tabular Dataset Models and Sentiment Analysis
 
JavaScript Development on Steroids with Oracle Visual Builder
JavaScript Development on Steroids with Oracle Visual BuilderJavaScript Development on Steroids with Oracle Visual Builder
JavaScript Development on Steroids with Oracle Visual Builder
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlowMachine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorFlow
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...
 Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor... Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensor...
 
Microservice Approach for Web Development with Micro Frontends
Microservice Approach for Web Development with Micro FrontendsMicroservice Approach for Web Development with Micro Frontends
Microservice Approach for Web Development with Micro Frontends
 
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and TensorflowMachine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflow
Machine Learning Applied - Contextual Chatbots Coding, Oracle JET and Tensorflow
 
Oracle Development Cloud Service
Oracle Development Cloud ServiceOracle Development Cloud Service
Oracle Development Cloud Service
 
Oracle Java Cloud Service: How to Estimate Production System Performance
Oracle Java Cloud Service: How to Estimate Production System PerformanceOracle Java Cloud Service: How to Estimate Production System Performance
Oracle Java Cloud Service: How to Estimate Production System Performance
 
Essential Kit for Oracle JET Programming
Essential Kit for Oracle JET ProgrammingEssential Kit for Oracle JET Programming
Essential Kit for Oracle JET Programming
 
Oracle JET and ADF BC REST Production Experience with Oracle Java Cloud
Oracle JET and ADF BC REST Production Experience with Oracle Java CloudOracle JET and ADF BC REST Production Experience with Oracle Java Cloud
Oracle JET and ADF BC REST Production Experience with Oracle Java Cloud
 
Offline Web with Oracle JET
Offline Web with Oracle JETOffline Web with Oracle JET
Offline Web with Oracle JET
 
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...
End-to-End Cloud: Oracle Java Cloud, Oracle Mobile Cloud Service, Oracle MAF,...
 
Forms, ADF and JET a Non-Aggression Pact
Forms, ADF and JET a Non-Aggression PactForms, ADF and JET a Non-Aggression Pact
Forms, ADF and JET a Non-Aggression Pact
 
Oracle JET CRUD and ADF BC REST
Oracle JET CRUD and ADF BC RESTOracle JET CRUD and ADF BC REST
Oracle JET CRUD and ADF BC REST
 
Oracle JET and WebSocket
Oracle JET and WebSocketOracle JET and WebSocket
Oracle JET and WebSocket
 
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI Support
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI SupportOracle Alta UI Patterns for Enterprise Applications and Responsive UI Support
Oracle Alta UI Patterns for Enterprise Applications and Responsive UI Support
 
ADF Mythbusters UKOUG'14
ADF Mythbusters UKOUG'14ADF Mythbusters UKOUG'14
ADF Mythbusters UKOUG'14
 
Data Caching Strategies for Oracle Mobile Application Framework
Data Caching Strategies for Oracle Mobile Application FrameworkData Caching Strategies for Oracle Mobile Application Framework
Data Caching Strategies for Oracle Mobile Application Framework
 
ADF Development Survival Kit
ADF Development Survival KitADF Development Survival Kit
ADF Development Survival Kit
 
ADF Anti-Patterns: Dangerous Tutorials
ADF Anti-Patterns: Dangerous TutorialsADF Anti-Patterns: Dangerous Tutorials
ADF Anti-Patterns: Dangerous Tutorials
 

Recently uploaded

(+971568250507 ))# Young Call Girls in Ajman By Pakistani Call Girls in ...
(+971568250507  ))#  Young Call Girls  in Ajman  By Pakistani Call Girls  in ...(+971568250507  ))#  Young Call Girls  in Ajman  By Pakistani Call Girls  in ...
(+971568250507 ))# Young Call Girls in Ajman By Pakistani Call Girls in ...Escorts Call Girls
 
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...SUHANI PANDEY
 
Top Rated Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...
Top Rated  Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...Top Rated  Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...
Top Rated Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...Call Girls in Nagpur High Profile
 
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)Delhi Call girls
 
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...nilamkumrai
 
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...tanu pandey
 
Al Barsha Night Partner +0567686026 Call Girls Dubai
Al Barsha Night Partner +0567686026 Call Girls  DubaiAl Barsha Night Partner +0567686026 Call Girls  Dubai
Al Barsha Night Partner +0567686026 Call Girls DubaiEscorts Call Girls
 
Trump Diapers Over Dems t shirts Sweatshirt
Trump Diapers Over Dems t shirts SweatshirtTrump Diapers Over Dems t shirts Sweatshirt
Trump Diapers Over Dems t shirts Sweatshirtrahman018755
 
Real Men Wear Diapers T Shirts sweatshirt
Real Men Wear Diapers T Shirts sweatshirtReal Men Wear Diapers T Shirts sweatshirt
Real Men Wear Diapers T Shirts sweatshirtrahman018755
 
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...SUHANI PANDEY
 
APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53APNIC
 
20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdf20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdfMatthew Sinclair
 
Microsoft Azure Arc Customer Deck Microsoft
Microsoft Azure Arc Customer Deck MicrosoftMicrosoft Azure Arc Customer Deck Microsoft
Microsoft Azure Arc Customer Deck MicrosoftAanSulistiyo
 
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...roncy bisnoi
 
"Boost Your Digital Presence: Partner with a Leading SEO Agency"
"Boost Your Digital Presence: Partner with a Leading SEO Agency""Boost Your Digital Presence: Partner with a Leading SEO Agency"
"Boost Your Digital Presence: Partner with a Leading SEO Agency"growthgrids
 
Katraj ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready For S...
Katraj ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready For S...Katraj ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready For S...
Katraj ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready For S...tanu pandey
 
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.soniya singh
 
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...Neha Pandey
 
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...nilamkumrai
 

Recently uploaded (20)

(+971568250507 ))# Young Call Girls in Ajman By Pakistani Call Girls in ...
(+971568250507  ))#  Young Call Girls  in Ajman  By Pakistani Call Girls  in ...(+971568250507  ))#  Young Call Girls  in Ajman  By Pakistani Call Girls  in ...
(+971568250507 ))# Young Call Girls in Ajman By Pakistani Call Girls in ...
 
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
Pirangut | Call Girls Pune Phone No 8005736733 Elite Escort Service Available...
 
Top Rated Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...
Top Rated  Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...Top Rated  Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...
Top Rated Pune Call Girls Daund ⟟ 6297143586 ⟟ Call Me For Genuine Sex Servi...
 
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)
WhatsApp 📞 8448380779 ✅Call Girls In Mamura Sector 66 ( Noida)
 
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
( Pune ) VIP Baner Call Girls 🎗️ 9352988975 Sizzling | Escorts | Girls Are Re...
 
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...Pune Airport ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready...
Pune Airport ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready...
 
Al Barsha Night Partner +0567686026 Call Girls Dubai
Al Barsha Night Partner +0567686026 Call Girls  DubaiAl Barsha Night Partner +0567686026 Call Girls  Dubai
Al Barsha Night Partner +0567686026 Call Girls Dubai
 
Trump Diapers Over Dems t shirts Sweatshirt
Trump Diapers Over Dems t shirts SweatshirtTrump Diapers Over Dems t shirts Sweatshirt
Trump Diapers Over Dems t shirts Sweatshirt
 
Real Men Wear Diapers T Shirts sweatshirt
Real Men Wear Diapers T Shirts sweatshirtReal Men Wear Diapers T Shirts sweatshirt
Real Men Wear Diapers T Shirts sweatshirt
 
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...
VVIP Pune Call Girls Sinhagad WhatSapp Number 8005736733 With Elite Staff And...
 
APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53APNIC Updates presented by Paul Wilson at ARIN 53
APNIC Updates presented by Paul Wilson at ARIN 53
 
20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdf20240508 QFM014 Elixir Reading List April 2024.pdf
20240508 QFM014 Elixir Reading List April 2024.pdf
 
Microsoft Azure Arc Customer Deck Microsoft
Microsoft Azure Arc Customer Deck MicrosoftMicrosoft Azure Arc Customer Deck Microsoft
Microsoft Azure Arc Customer Deck Microsoft
 
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...
Call Girls Sangvi Call Me 7737669865 Budget Friendly No Advance BookingCall G...
 
"Boost Your Digital Presence: Partner with a Leading SEO Agency"
"Boost Your Digital Presence: Partner with a Leading SEO Agency""Boost Your Digital Presence: Partner with a Leading SEO Agency"
"Boost Your Digital Presence: Partner with a Leading SEO Agency"
 
Katraj ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready For S...
Katraj ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready For S...Katraj ( Call Girls ) Pune  6297143586  Hot Model With Sexy Bhabi Ready For S...
Katraj ( Call Girls ) Pune 6297143586 Hot Model With Sexy Bhabi Ready For S...
 
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.
Call Now ☎ 8264348440 !! Call Girls in Green Park Escort Service Delhi N.C.R.
 
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
valsad Escorts Service ☎️ 6378878445 ( Sakshi Sinha ) High Profile Call Girls...
 
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
𓀤Call On 7877925207 𓀤 Ahmedguda Call Girls Hot Model With Sexy Bhabi Ready Fo...
 
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...
( Pune ) VIP Pimpri Chinchwad Call Girls 🎗️ 9352988975 Sizzling | Escorts | G...
 

Reliable, Fast, Engaging Offline-First Architecture for JavaScript Applications

  • 1. Reliable, Fast, Engaging Offline-First Architecture for JavaScript Applications Andrejus Baranovskis, CEO andTechnical Expert, Red Samurai Consulting Oracle ACE Director and Oracle Groundbreaker Ambassador Florin Marcus,Technical Expert, Red Samurai Consulting
  • 2. Oracle ExpertsTeam ADF, JET, ORACLE FUSION, ORACLE CLOUD, MACHINE LEARNING Oracle PaaS Partner Community Award for Outstanding Java Cloud Service Contribution 2017
  • 3. AGENDA • Offline-First Architecture Why and How • Technical Implementation • Solution WalkThrough • Offline PersistenceToolkit Config for JavaScript (Oracle JET)
  • 5. No Wi-Fi? No Problem Build web/mobile applications that can work great, whatever the connection
  • 7. Oracle JET Offline PersistenceToolkit Online Log Request for Replay Local Storage for Temporary Data request/response yes no
  • 8. KEY POINTS • Business logic code stays the same, simple for developers, no code intrusion with offline toolkit API • API is very rich, many options to override and control • Configurable and flexible, e.g. store factory can be changed • API is heavily based on JS Promise - represents the eventual completion (or failure) of an asynchronous operation, and its resulting value
  • 9. KEY POINTS • Offline: • Request is logged and inserted into replay queue automatically • If user is changing data, developer must implement request update logic - in custom handlePatch or handlePost methods • Online: • Possible to define before and after synch listeners.This helps to control request data • Can control request synchronisation queue execution
  • 11. ENDPOINT REGISTRATION Register Endpoint Define Shredder and Query Handler Provide handlePatch method Define before/after listeners
  • 12. ENDPOINT REGISTRATION persistenceManager.init().then(function() { persistenceManager.register({scope: '/Employees'}) .then(function(registration) { var responseProxy = defaultResponseProxy.getResponseProxy({ jsonProcessor: { shredder: oracleRestJsonShredding.getShredder('emp', 'EmployeeId'), unshredder: oracleRestJsonShredding.getUnshredder() }, queryHandler: queryHandlers.getOracleRestQueryHandler('emp'), requestHandlerOverride: {handlePatch: customHandlePatch} }); var fetchListener = responseProxy.getFetchEventListener(); registration.addEventListener('fetch', fetchListener); // initial data load self.fetchData(); }); // handles request data before synch persistenceManager.getSyncManager().addEventListener('beforeSyncRequest', self.beforeRequestListener, '/Employees' ); // handles response data after synch persistenceManager.getSyncManager().addEventListener('syncRequest', self.afterRequestListener, '/Employees' ); });
  • 13. OFFLINE UPDATE OR CREATION Offline yes Get request data Open offline store Find data by key in offline store Update data in offline store
  • 14. OFFLINE UPDATE OR CREATION var customHandlePatch = function(request) { if (!persistenceManager.isOnline()) { persistenceUtils.requestToJSON(request).then(function (data) { var requestData = JSON.parse(data.body.text); persistenceStoreManager.openStore('emp').then(function (store) { store.findByKey(requestData.EmployeeId).then(function (data) { data.FirstName = requestData.FirstName; data.ChangeIndicatorAttr = requestData.ChangeIndicatorAttr; store.upsert(requestData.EmployeeId, JSON.parse('{}'), data); }) }); }) var init = {'status': 503, 'statusText': 'Edit will be processed when online'}; return Promise.resolve(new Response(null, init)); } else { return persistenceManager.browserFetch(request); } };
  • 15. ONLINE REPLAY Check if request should be replayed Execute replay If replay error occurs, ask user feedback and read info from response Clear up helper variables when replay completes
  • 16. ONLINE REPLAY persistenceManager.getSyncManager().sync({preflightOptionsRequest: 'disabled'}).then(function () { employeeSynchMap = {}; console.log('SYNCH DONE'); }, function (error) { var statusCode = error.response.status; if (statusCode == 409) { // conflict during offline data synch $("#md3").ojDialog("open"); synchErrorRequestId = error.requestId; var response = error.response; response.json().then(function (value) { self.onlineConflictResolutionTitle('Conflict resolution: ' + value.FirstName); synchErrorChangeIndicatorAttr = value.ChangeIndicatorAttr; }); } } );
  • 17. AFTER REQUEST LISTENER Get response data Update local change indicator value with the one from response Save data ID and change indicator value
  • 18. AFTER REQUEST LISTENER self.afterRequestListener = function (event) { // invoked if offline synch for request was success, to bring back values updated in backend var statusCode = event.response.status; if (statusCode == 200) { event.response.json().then(function(response) { var id = response.EmployeeId; var changeIndicatorAttr = response.ChangeIndicatorAttr; for (var i = 0; i < self.allItems().length; i++) { if (self.allItems()[i].id === id) { self.allItems.splice(i, 1, {"id": self.allItems()[i].id, "firstName": self.allItems()[i].firstName, "lastName": self.allItems()[i].lastName, "email": self.allItems()[i].email, "phoneNumber": self.allItems()[i].phoneNumber, "changeIndicatorAttr": changeIndicatorAttr}); employeeSynchMap[id] = changeIndicatorAttr; console.log('UPDATE SUCCESS IN SYNCH FOR: ' + id + ',WITH NEW CHANGE INDICATOR: ' + changeIndicatorAttr); break; } } }); } return Promise.resolve({action: 'continue'}); }
  • 19. BEFORE REQUEST LISTENER Construct request promise Get request data Check if change indicator value must be updated Update request and continue
  • 20. BEFORE REQUEST LISTENER self.beforeRequestListener = function (event) { var request = event.request; return new Promise(function (resolve, reject) { persistenceUtils.requestToJSON(request).then(function (data) { var empl = JSON.parse(data.body.text); var employeeId = empl.EmployeeId; var updateRequest = false; for (var i in employeeSynchMap) { if (parseInt(i) === parseInt(employeeId)) { updateRequest = true; var changeIndicatorVal = employeeSynchMap[i]; empl.ChangeIndicatorAttr = changeIndicatorVal; data.body.text = JSON.stringify(empl); persistenceUtils.requestFromJSON(data).then(function (updatedRequest) { resolve({action: 'replay', request: updatedRequest}); }); break; }} if (!updateRequest) { resolve({action: 'continue'}); }
  • 21. CONFLICT - APPLY CLIENT CHANGES Remove request Read request data Update change indicator value and construct new request Insert new request into queue and call replay
  • 22. CONFLICT - APPLY CLIENT CHANGES self.applyOfflineClientChangesToServer = function(event) { persistenceManager.getSyncManager().removeRequest(synchErrorRequestId).then(function (request) { $("#md3").ojDialog("close"); persistenceUtils.requestToJSON(request).then(function (requestData) { var requestPayload = JSON.parse(requestData.body.text); requestPayload.ChangeIndicatorAttr = synchErrorChangeIndicatorAttr; requestData.body.text = JSON.stringify(requestPayload); persistenceUtils.requestFromJSON(requestData).then(function (request) { persistenceManager.getSyncManager().insertRequest(request).then(function () { self.synchOfflineChanges(); }); }); }); }); }
  • 23. CONFLICT - APPLY SERVER CHANGES Refresh client side entry by calling backend service Call replay to continue Remove request Read request data
  • 24. CONFLICT - APPLY SERVER CHANGES self.cancelOfflineClientChangesToServer = function(event) { persistenceManager.getSyncManager().removeRequest(synchErrorRequestId).then(function (request) { $("#md3").ojDialog("close"); persistenceUtils.requestToJSON(request).then(function (requestData) { var requestPayload = JSON.parse(requestData.body.text); var employeeId = requestPayload.EmployeeId; var searchUrl = empls.getEmployeesEndpointURL() + "/" + employeeId; self.refreshEntry(searchUrl); }); self.synchOfflineChanges(); }); }
  • 25. OFFLINE PERSISTENCETOOLKIT CONFIG FOR JAVASCRIPT (ORACLE JET)
  • 26. PATH MAPPING: OFFLINETOOLKIT "offline": { "cdn": "", "cwd": "node_modules/@oracle/offline-persistence-toolkit/dist/debug", "debug": { "src": ["*.js", "impl/*.js"], "path": "libs/offline-persistence-toolkit/v1.1.5/dist", "cdnPath": "" } }
  • 27. PATH MAPPING: POUCH DB "pouchdb": { "cdn": "", "cwd": "node_modules/pouchdb/dist", "debug": { "src": ["*.js"], "path": "libs/pouchdb/v6.3.4/dist/pouchdb.js", "cdnPath": "" } } require(['pouchdb'], function (pouchdb) { window.PouchDB = pouchdb; });
  • 28. DEFINE BLOCK FOR OFFLINETOOLKIT define(['ojs/ojcore', 'knockout', 'jquery', 'offline/persistenceStoreManager', 'offline/pouchDBPersistenceStoreFactory', 'offline/persistenceManager', 'offline/defaultResponseProxy', 'offline/oracleRestJsonShredding', 'offline/queryHandlers', 'offline/persistenceUtils', 'offline/impl/logger', 'viewModels/helpers/employeesHelper',
  • 30. CONTACTS • Andrejus Baranovskis (https://andrejusb.blogspot.com) • Email: abaranovskis@redsamuraiconsulting.com • Twitter: @andrejusb • LinkedIn: https://www.linkedin.com/in/andrejus-baranovskis-251b392 • Web: http://redsamuraiconsulting.com
  • 31. REFERENCES • Source Code on GitHub - https://bit.ly/2PU4iaw • Oracle OfflineToolkit on GitHub - https://bit.ly/2I7Nr11 • Oracle JET - https://bit.ly/2O21GtS