Is it safe to override window.sessionStorage.getItem and setItem? - javascript

In my SPFx web part I am using an npm package that defines React.js controls that I need. The library is https://github.com/pnp/sp-dev-fx-controls-react, but my question is not strictly related to this library.
For specific reasons I need to use an older version of this library.
One of the controls I use reads data from the server and caches it in session storage (it's the TaxonomyPicker). Now in my use case, this data can be more than 7MB in size and the code fails where it tries to call setItem(key, value) with this long string. Unfortunately this error leads to the control not rendering.
Since I didn't want to change the (old version of the) library I am using, I decided to override the window.sessionStorage.getItem and setItem methods by functions that compress the string before it is saved and decompress it after it has been read with getItem. I do this only for large strings. Here's my implementation (in TypeScript):
fixSetItemFunc = (): void => {
const lzPrefix = 'lz:';
const threshold = 5000;
if (!this.oldSetItemFunc) {
this.oldSetItemFunc = window.sessionStorage.setItem.bind(window.sessionStorage);
const setItem = (key: string, value: string) => {
let compressedValue: string;
if (value && value.length > threshold) {
compressedValue = lzPrefix + LZ.compress(value);
}
else {
compressedValue = value;
}
this.oldSetItemFunc(key, compressedValue);
};
window.sessionStorage.setItem = setItem;
this.oldGetItemFunc = window.sessionStorage.getItem.bind(window.sessionStorage);
const getItem = (key: string) => {
const value = this.oldGetItemFunc(key);
let decompressedValue: string;
if (value && value.length && value.substring(0, lzPrefix.length) === lzPrefix) {
decompressedValue = LZ.decompress(value.substring(lzPrefix.length));
}
else {
decompressedValue = value;
}
return decompressedValue;
};
window.sessionStorage.getItem = getItem;
}
}
Now the interesting thing is that this workaround seems to work. But since session storage can live quite a while and users can navigate away from the page where I add the override, I am wondering if this is in anyway a reliable solution. Because all other code that is run on other SharePoint pages will use my overridden methods after I called fixSetItemFunc() once in my web part.
So I would be interested in your thoughts on this.

As you said, this override will apply and could potentially affect all the code on the page, not just your code. Why don't you instead break the long string into fragments and store those fragments as separate keys into the sessionStorage following some smart naming convention for the keys? That way you could easily isolate the logic for handling of large strings to your code only, without having to override a global method.
Btw, keep in mind that the sessionStorage is an in-memory storage private to the current browser window/tab, so every window/tab will have to maintain its own copy of the data.

Related

How would you be able to use fast-json package in the browser?

I have been browsing ways on how I could improve my apps performance and came across these 2 packages. I am building a forum styled app that receives a bunch of information from APIs and regularly parses and stringifies it. I have already optimized my front end JS as much as I could(it's just vanilla JS at this point) but it sometimes still struggles to load on older phones. I would love to try these packages as they could potentially relieve my issue but they are both in NPM form.
I would like to use them in my browser UI scripts.Is there any way to use NPM packages directly in browser/ can you get a single script version of them?
Specifically:
https://www.npmjs.com/package/fast-json
https://www.npmjs.com/package/fast-json-stringify
Very simple!
Copy the code from fast-json/blob/master/lib/EventTree.js in your source code, but without module.exports block (is to bottom of the code)
Copy the code from fast-json/blob/master/lib/FastJson.js in your source code, but without const { EventTree } = require('./EventTree'); (at top) and without module.exports = FastJson; (bottom)
TEST TIME ^_^
const fastJson = new FastJson();
// Path is a string representing a javascript object path
fastJson.on('ireland.people', (value) => {
console.log('ireland.people ->', value);
});
// Paths can be also an array of keys
fastJson.on(['spain', 'people', '1', 'name'], (value) => {
console.log(['spain', 'people', '1','name'], '->', value);
});
// Wildcards can be used to match all items in object or array
fastJson.on('spain.people[*].name', (value) => {
console.log('spain.people[*].name ->', value);
});
fastJson.on('*.people[*].name', (value) => {
console.log('*.people[*].name ->', value);
});
fastJson.write(data);

Making a custom group of defined chaining methods in js

The question is related to general js programming, but I'll use nightwatch.js as an example to elaborate my query.
NightWatch JS provides various chaining methods for its browser components, like: -
browser
.setValue('input[name='email']','example#mail.com')
.setValue('input[name='password']', '123456')
.click('#submitButton')
But if I'm writing method to select an option from dropdown, it requires multiple steps, and if there are multiple dropdowns in a form, it gets really confusing, like: -
browser
.click(`#country`)
.waitForElementVisible(`#india`)
.click(`#india`)
.click(`#state`)
.waitForElementVisible(`#delhi`)
.click(`#delhi`)
Is it possible to create a custom chaining method to group these already defined methods? For example something like:
/* custom method */
const dropdownSelector = (id, value) {
return this
.click(`${id}`).
.waitForElementVisible(`${value}`)
.click(`${value}`)
}
/* So it can be used as a chaining method */
browser
.dropdownSelector('country', 'india')
.dropdownSelector('state', 'delhi')
Or is there any other way I can solve my problem of increasing reusability and readability of my code?
I'm somewhat new to JS so couldn't tell you an ideal code solution, would have to admit I don't know what a proxy is in this context. But in the world of Nightwatch and test-automation i'd normally wrap multiple steps I plan on reusing into a page object. Create a new file in a pageObject folder and fill it with the method you want to reuse
So your test...
browser
.click(`#country`)
.waitForElementVisible(`#india`)
.click(`#india`)
.click(`#state`)
.waitForElementVisible(`#delhi`)
.click(`#delhi`)
becomes a page object method in another file called 'myObject' like...
selectLocation(browser, country, state, city) {
browser
.click(`#country`) <== assume this never changes?
.waitForElementVisible(country)
.click(country)
.click(state)
.waitForElementVisible(city)
.click(city);
}
and then each of your tests inherit the method and define those values themselves, however you chose to manage that...
const myObject = require ('<path to the new pageObject file>')
module.exports = {
'someTest': function (browser) {
const country = 'something'
const state = 'something'
const city = 'something'
myObject.selectLocation(browser);
You can also set your country / state / city as variables in a globals file and set them as same for everything but I don't know how granular you want to be.
Hope that made some sense :)
This is a great place to use a Proxy. Given some class:
function Apple ()
{
this.eat = function ()
{
console.log("I was eaten!");
return this;
}
this.nomnom = function ()
{
console.log("Nom nom!");
return this;
}
}
And a set of "extension methods":
const appleExtensions =
{
eatAndNomnom ()
{
this.eat().nomnom();
return this;
}
}
We can create function which returns a Proxy to select which properties are retrieved from the extension object and which are retrieved from the originating object:
function makeExtendedTarget(target, extensions)
{
return new Proxy(target,
{
get (obj, prop)
{
if (prop in extensions)
{
return extensions[prop];
}
return obj[prop];
}
});
}
And we can use it like so:
let apple = makeExtendedTarget(new Apple(), appleExtensions);
apple
.eatAndNomnom()
.eat();
// => "I was eaten!"
// "Nom nom!"
// "I was eaten!"
Of course, this requires you to call makeExtendedTarget whenever you want to create a new Apple. However, I would consider this a plus, as it makes it abundantly clear you are created an extended object, and to expect to be able to call methods not normally available on the class API.
Of course, whether or not you should be doing this is an entirely different discussion!

AEM 6.2 get component property from parent page

I have a footer element which needs to be shared. My plan is to set up the footer on the parent/home page but allow child pages to overwrite those properties.
I first look on the current component for the property (pretty standard), after that I get the path to the parent page to look for the property on the component with the same name.
function getProperty(property, currentPage) {
var val = null,
page = currentPage,
rootPage = page.getAbsoluteParent(2);
var curNode = currentNode.getPath(),
nodeStrIdx = curNode.indexOf("jcr:content"),
nodeStr = curNode.substr(nodeStrIdx + 12); // Remove 'jcr:content/' too
while(val == null) {
// If we've gone higher than the home page, return
if(page.getDepth() < 3) {
break;
}
// Get the same node on this page
var resource = page.getContentResource(nodeStr);
if(resource != null) {
var node = resource.adaptTo(Node.class); // *** This is null ***
// val = node.get(property);
}
// Get the parent page
page = page.getParent();
}
return val;
}
I've seen that you can change the type of the content resource to a node which should allow me to get the same property but the resource.adaptTo(Node.class) is returning null.
In case it's not clear, resource is an absolute path to the node I want to pull the property from e.g. /content/jdf/en/resources/challenge-cards/jcr:content/footer/follow-us
Assuming you are using Javascript HTL Use API, you need to use fully qualified name for Java class, like this:
var node = resource.adaptTo(Packages.javax.jcr.Node);
Then you can retrieve your value in this way:
if (node.hasProperty(property)) {
val = node.getProperty(property).getString();
}
You need to use hasProperty method per Node API as getProperty throws PathNotFoundException when a property is missing. You also need to be careful with granite.resource object from examples - it is not the same Resource object and it does not have adaptTo method. To get to the original resource of the component you need to take nativeResource property:
var node = granite.resource.nativeResource.adaptTo(Packages.javax.jcr.Node);
However, there also should be a faster way to get property from the resource in JS:
val = resource.properties[property];
As this is a quite common case to develop components properties inheritance, you can also consider some ready-to-use solutions like HierarchyNodeInheritanceValueMap API or Inheritance Paragraph System (iparsys) in your implementation design.
As this JS is server-side compiled with Mozilla Rhino, all these objects and methods used here are Java objects and methods so that you should be also able to use HierarchyNodeInheritanceValueMap in this way:
//importClass(Packages.com.day.cq.commons.inherit.HierarchyNodeInheritanceValueMap);
//this import might be needed but not necessarily
var props = new HierarchyNodeInheritanceValueMap(granite.resource.nativeResource);
val = props.getInherited(property, Packages.java.lang.String);
It will then return to val the property value for the current resource, or, if empty, the property value of the resource at the same position on the parent page, or if empty etc. These two lines should do all the magic then.

Check if data came back from firebase using angular2

I have the following code to load data from my firebase which works but it is painfully ugly.
loadData() {
// app.component
this.loadJobCodeSubscription = this.databaseService.getData()
.subscribe(result => {
this.data = null;
var data_raw: Array<any>;
if (Object.keys(result)[0] != '$value') {
data_raw = Object.keys(result).map(function (key) {
return [key, result[key]['firstName'], result[key]['lastName']];
})
// remove extra data downloaded from firebase
jobDataRaw.pop();
jobDataRaw.pop();
}
this.jobCodeData = jobDataRaw;
}, error => {
console.log('Error downloading job codes', error)
})
}
// DatabaseService
getData() {
return this.af.database.object('/jobCodes/' + this.currentUser.company)
}
What happens is that if the branch I am querying does not have data, my "result" will display
Andi if I have data coming back, I will get something like
The only difference (that I can think of to check for) between these two data is that one has a $value key and one does not
That is why I am using that weird if statement to check if that key exists.
Is there a way to check for data in a neater way?
Also, is there a way to cast 'result' to the right format as oppose to 'any' which it currently is
Note. I am using Angular 2 with AngularFire2
First, presuming you have control of the backend, you could alter the return to be more easily understandable. Returning an empty payload is a bit confusing if you really mean "nothing".
Second, dangerous to do this line:
Object.keys(result)[0]
As Object.keys(result) may not be the same in the future, even though it may be deterministic.
I think the safest way to do this would be:
if (Object.keys(result).every(k => k != '$value')) {
That being said, I don't think there's an easier way to determine that given the information you've presented. If you continue with this approach, it would be good to make sure you are guarding your statements carefully, as it seems like
bad data could slip through in the future.
Turns out that if I change the return type of the observable to be the original firebase snapshot, the data looks much better. It allows me to call if(snapshot.val()){}
return this.af.database.object('/jobCodes/' + this.currentUser.company, {preserveSnapshot: true})

Using flow types on a dynamically built object

I'm starting to use flow type checker on my javascript code. I tend to be quite functional using javascript and I often build APIs dinamically.
For example, this is quite common on my code:
const levels = [`error`, `warn`, `log`, `info`, `debug`];
const API = Object.create(null);
API.flush = flush.bind(API, false);
// Here I'm attaching the rest of the methods to the api
levels.reduce(
(api, name, lvl) => {
api[name] = handleLogMessage.bind(api, name, lvl);
return api;
}, API);
Now if I want to cover this interface with Flow I try to do something like this:
type iTracer = {
flush(): void,
warn(): void
};
But I get several errors, like this one: ´Property not found in possibly null value null´ or weirder: Covariant propertywarnincompatible with contravariant use in assignment of computed property/element
Isn't flow supposed to understand the dynamic nature of javascript and flow with it? To be honest, it is dramatically reducing my productivity, instead of boosting it.
Any help with this will be much appreciated

Categories

Resources