Track user time in completing a particular action in a website - javascript

I want to track how much time user is taking in completing a particular action (including server response time and render time(DOM related changes )) in website.
I have tried it in Angular framework. To do it, I am thinking of recording the time when user started the action and I want to note the time when the action is completed. As a developer, I will know when user started the activity and when user finish the action like search, filter, edit, add, delete etc. So, we can take the difference b/w them. But to note every action, we have to write code in every part of the app. Can we create a plugin so that we can use it everywhere instead of writing same code everywhere to track the time of user. Any approach to create it? Or is there any tool available to achieve this feature?

Would something like this help?
#Injectable({provideIn: 'root'})
export class TrackingService {
private cache: {[id: number]: {description: string, time: number}} = {};
private id: number = 0;
public startTracking(actionDescription: string): number{
const id = ++this.id;
this.cache[id] = { description: actionDescription, time: new Date().getTime() };
return id;
}
public stopTracking(actionId: number){
const data = this.cache[actionId];
if(data){
const elapsed = new Date().getTime() - data.time;
// ...
// Do something with your 'elapsed' and 'data.description'
// ...
delete this.cache[id];
return {...data, elapsed: elapsed};
}
throw `No action with id [${actionId}] running! `;
}
}
Ad then anywhere you need to track an action:
private actionId: number;
constructor(private trackingService: TrackingService){}
startAction(){
this.actionId = this.trackingService.startTracking('Description');
}
stopAction(){
const trackingResult = this.trackingService.stopTracking(this.actionId);
}
You can automate the tracking in some places, for example for routing:
// app.module.ts
private routeChangeSubscription: Subscription;
private configLoadActionId: number;
private navigationActionId: number;
constructor(private router: Router, private trackingService: TrackingService){
this.routeChangeSubscription = router.events.subscribe((event: Event) => {
if (event instanceof RouteConfigLoadStart) {
this.configLoadActionId = this.trackingService.startTracking('configLoad');
}
else if (event instanceof RouteConfigLoadEnd) {
const result = this.trackingService.stopTracking(this.configLoadActionId);
// ... process the result if you wish
}
else if (event instanceof NavigationStart) {
this.navigationActionId = this.trackingService.startTracking('navigation');
}
else if (event instanceof NavigationEnd) {
const result = this.trackingService.stopTracking(this.navigationActionId);
// ... process the result if you wish
}
});
}
Or for HTTP requests:
// http-tracking.interceptor
export class HttpTrackingInterceptor implements HttpInterceptor {
constructor(private trackingService: TrackingService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const actionId = this.trackingService.startTracking('HTTP request');
return next.handle(req.clone()).pipe(
tap(r => this.trackingService.stopTracking(actionId))
);
}
}
// app.module.ts
#NgModule({
// ... other module stuff
providers: [
// ... other providers
{
provide: HTTP_INTERCEPTORS,
useClass: HttpTrackingInterceptor,
multi: true,
deps: [TrackingService]
}
]
})
export class AppModule { ... }
You can easily extend the TrackingService to return Promises or Observables or whatever else, in case you prefer that...
Hope this helps a little :-)

Can we create a plugin so that we can use it everywhere instead of
writing same code everywhere to track the time of user. Any approach
to create it? Or is there any tool available to achieve this feature?
It's a very important Feature Request by many. So, I write a detailed, working and simple solution on the subject here.
#himanshu-garg You are requesting a feature already created for this workflow. It's a plugin you can include in any website. It's none other than activity tracking in timeonsite.js
Look at the following code,
<head>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/timeonsite/1.2.0/timeonsitetracker.js"></script>
<script>
var config = {
// track page by seconds. Default tracking is by milliseconds
trackBy: 'seconds',
callback: function(data) { /* callback denotes your data tracking is real-time */
console.log(data);
var endPointUrl = 'http://example.com' //Replace with your actual backend API URL http://localhost/tos
if (data && data.trackingType) {
if (data.trackingType == 'tos') {
if (Tos.verifyData(data) != 'valid') {
console.log('Data abolished!');
return;
}
}
// make use of sendBeacon if this API is supported by your browser.
if (navigator && typeof navigator.sendBeacon === 'function') {
data.trasferredWith = 'sendBeacon';
var blob = new Blob([JSON.stringify(data)], {type : 'application/json'});
navigator.sendBeacon(endPointUrl, blob);
}
}
}
};
var Tos;
if (TimeOnSiteTracker) {
Tos = new TimeOnSiteTracker(config);
}
</script>
</head>
Then, when the user clicks on a specific action in the site, for example "edit the post" or "click on the create post",
You just initiate the Tos.startActivity() API like,
Tos.startActivity({actionPerfomed: 'Edit a post'});
Then when the user completes the edit or create post actions and when he finally clicks the "save/submit" button, you trigger the Tos.endActivity() API like,
Tos.endActivity({customData: 'custom data if any here...'});
You'll see following object directly saved into your table,
{
TOSId: 585872449448,
TOSSessionKey: "14802525481391382263",
TOSUserId: "anonymous",
title: "Test application - TimeOnSiteTracker",
URL: "http://example.com/post/nature-is-beautiful/edit.php",
activityStart: "2021-11-27 13:20:46.707",
activityEnd: "2021-11-27 13:20:50.213",
timeTaken:4,
timeTakenByDuration: "0d 00h 00m 04s"
timeTakenTrackedBy: "second",
trackingType: "activity",
actionPerfomed: "Edit a post", //optional fields
customData: "custom data if any here..." //optional fields
}
As you can see, the actions
"Edit/Create post" is captured
"timeTaken" is captured in seconds/milliseconds depending upon configuration
"type:activity" is captured
"activityStart" is captured
"activityEnd" is captured
"TOSUserId" // who does the action along with TOSSessionKey to uniquely identify the session.
What else you need? Since it's stored in SQL DB table, you can do analysis/reporting queries yourself and take it to top-level management for decisions. The same is the case for NoSQL as well. Timeonsite.js is supporting both RDBMS and NoSql DB types.
On top of it, 1.Minimize tab, 2.Inactive tab and 3.Switch tab's idle time are all computed and ignored automatically by the tracker itself.
This tracker can be plugged-in in any library Angular, React, Jquery etc. since it's plain vanilla JS library.
Let me know if you need more input on the subject. I can assist you on this.

You have to write a simple Event Tracker in your client code. Since I don't know which events you want to track, I'll provide the solution for a general case.
Also, you'll have to manually trigger the start and stop tracking.
EventTracker = {
trackedEvents: {},
start: function(key) {
var startTime = new Date();
this.trackedEvents[key] = {
start: startTime
}
},
stop: function(key) {
var endTime = new Date();
this.trackedEvents[key]['duration'] = (endTime - this.trackedEvents[key]['start']) / 1000 + 's';
this.trackedEvents[key]['end'] = endTime;
},
}
// Use EventTracker everywhere to track performance
// Example:
EventTracker.start('search_track'); // User searches, start tracking.
setTimeout(function() {
EventTracker.stop('search_track'); // Records fetched after 5 seconds. Stop tracking.
console.log(EventTracker.trackedEvents);
}, 5000);
You can track all events according to your need. For server response, use: EventTracker.start('search_ajax_track') when you make the request and stop the tracking when you get the response.
You can modify above code to measure other parameters according to your requirements.

I am going to recommend you use custom Google Analytics events. In particular User Timings. This allows you to log specific timings on your webpage, you can log with your own labels and categories.
To quote the documentation:
User timings allow developers to measure periods of time using the
analytics.js library. This is particularly useful for developers to
measure the latency, or time spent, making AJAX requests and loading
web resources.
I have some sample code below, this just hooks into clicks, and will get a descriptor from attribute data-name - if not available will just log as 'Anonymous Click' - you can customise this to not track unmarked items. You can also hook into ajax calls and other notable events, without knowing your specific requirements it's hard to give further examples.
Example markup helper to lock click events.
<button data-name="Foo"/>
The below code does the logging, note that it logs using window.performance.now() - which will return the time from when the page was loaded in milliseconds. This will allow you to generate a timeline of user interactions as opposed to getting raw time spent on a single task, which by the way Google Analytics reports can calculate for you.
(function($, Analytics) {
init_hooks();
function init_hooks() {
$('body').on('click', track);
}
function track(e) {
// Get a name to record this against
var name = e.target.data(name) || "Anonymous Click";
// Time since page loaded
var time = window.performance.now()
Analytics('send', {
hitType: 'timing',
timingCategory: 'Front End Intereactions',
timingVar: name,
timingValue: time
});
}
})(jQuery, ga)
Find out more look at the docs.

You could instrument your code with OpenTracing for Js.
You will need to add a request in your transaction start and end.
Also a OpenTracing server to receive request from the browser.

Related

Hide REST and Websockets inside the same function using a listener callback for both

I am working on a project with 2 clients, 1 in React Native and 1 in React. There is a common API module, which is auto-generated by an OpenAPI specification, and a Services module that is responsible for calling the API methods and translating the DTOs to a more suitable format.
The app has real-time requirements and fetching is carried out in the following manner:
Let R be a resource on the server with a structure like { ...fields..., timestamp: 12345678 }.
Let N be the timestamp of now. The client apps need to fetch all the R resources that have a timestamp higher than N - 10 hours and also subscribe to get new R items, when and if any are created (through Websockets). If the user of the app is currently on a screen that only concerns old data (N - 10 hours <= timestamp <= N), then the Websocket subscription should be omitted.
My question is, how bad would it be if I implemented methods like the following where I completely disregard returning Promises, like when someone uses fetch, and I use callbacks to inform the user of the method that the "Promise resolved"?
type R = {
timestamp: Date
}
type API = {
getR: (start: Date, end: Date) => R[];
subscribeToR: (onNewR: (r: R) => void) => void;
}
class NewServices {
constructor(private readonly api: API) {}
// notice that end is optional and onNewR receives an array
getR(onNewR: (r: R[]) => void, start: Date, end?: Date): void {
if (end !== undefined) {
this.api.getR(start, end).then((resp) => {
onNewR(resp.data);
});
return;
}
// end is undefined here
const lEnd = new Date(); // now
this.api.getR(start, lEnd).then((resp) => {
onNewR(resp.data);
this.api.subscribeToR((r) => onNewR([r]));
});
}
}
// The current state of the services
class OldServices {
constructor(private readonly api: API) {}
// This class is an example, IRL it does DTO conversions, too
async getR(start: Date, end: Date): Promise<R[]> {
return (await this.api.getR(start, end)).data;
}
subscribeToR(onNewR: (r: R) => void): void {
this.api.subscribeToR(onNewR);
}
}
The apparent drawback is that I can no longer use React Query, which I liked.
However, I got tired of writing the if (end !== undefined) logic in the (thankfully reusable) business layer of my app and, also, I believe that this "either REST or Websocket" behavior should be abstracted out from the rest of the app and be hidden inside the Services layer, even though it feels like an anti-pattern. What do you suggest/think?

How to create subset of observable basing on input argument?

I know the title might sound confusing, but I'm not sure how to express it accurately.
Let me explain the use case:
We've got a calendar with recurring events (potentially infinite list of events repeating eg every Monday forever)
I've got a bunch of UI elements interested in some part of those events (eg. some needs events from next week, some from next month, some from the previous year - they all might be displayed at once). Note that those requirements might overlap - the eg. current week and current month actually include an overlapping set of days, but those UI components are not aware of each other.
I need to refresh those events on some actions in the app (eg. the user changed calendar permission, list of calendars in his system, user minimized and re-opened the app) - I've got streams of those events like $appStatus $permissionStatus, $selectedCalendars
Fetching events is expensive and I want to re-use fetching and existing data as much as possible. Eg. if 3 components need events from the same week, I don't want to make 3 requests.
Also if one component requests events from March and another from 2nd week of March, I'd like to solve it with one request if possible. Note they might start their request at different moment.
I also don't want to fetch events that are not requested anymore (eg. part of UI showing events from the previous month is not rendered anymore)
I've got a few ideas about how to do it in plain JS, but I wonder if it's possible to create RxJs-ish solution for that
My raw-js-rx-mix ideas:
When a new range is requested, I 'round' it to full weeks. Then I create new observable with shared value for each week and remember it in some map
eg
const observablesMap = new Map();
function getObservableForDate(date: Date) {
const weekStart = getStartOfWeek(date);
// if we have 'cached' version of observable for this week - return cached one
if (observablesMap.has(weekStart.getTime()) {
return observablesMap.get(weekStart.getTime())
}
const weekObservable = // create observable that shares results
// set it to the map
// return it
}
But after a few hours of research, I have no idea if and how would I implement it in RxJS way.
Let's assume the fetching function signature is fetchEvents(startDate, endDate)
ps. I don't expect a working solution code, but just some guide. I've checked most of RxJS documentation and could not find anything promising for such use case
I created a running rxjs solution on stackblitz. I will also add the code here in case stackblitz shuts down one time in the future.
General idea
Save all requests and decide if you need a new request or not
Depending on previous decicion fake or process http request
Depending on the previous kind of request find the existing request or return the newly requested one.
If there need to be further infos please let me know and I try to explain in detail or add comments. I did not implement 100% of your requirements but with the following solution as base it should be possible to expand interfaces and functions within the pipes to implement them. Also if you see any code redundancy or optimization to interfaces, let me know and I will adapt
Interfaces
interface Requests {
action: Action,
currentRequest: number,
accumulatedRequests: number[],
}
interface FulfilledRequest extends Requests{
httpRequest: string
}
interface Response {
accumulatedHttpResponses: {
request: number,
response: string
}[],
response: string
}
Default Values
const defaultRequests: Requests = {
action: Action.IgnoreRequest,
currentRequest: -1,
accumulatedRequests: []
}
const defaultResponseStorage: Response = {
accumulatedHttpResponses: [],
response: ''
}
Enum
enum Action {
IgnoreRequest,
ProcessRequest
}
Functions
const isUpdateAction = (action: Action) => action === Action.ProcessRequest
const fakeHttp = (date: number): Observable<string> => of('http response for: ' + date).pipe(
tap(v => console.warn('fakeHttp called with: ', v))
);
const getResponseForExistingRequest = (storage: Response, request: FulfilledRequest): Response => {
const index = storage.accumulatedHttpResponses.findIndex(response => response.request === request.currentRequest);
return {
accumulatedHttpResponses: storage.accumulatedHttpResponses,
response: storage.accumulatedHttpResponses[index].response
}
}
const getResponseForNewRequest = (storage: Response, request: FulfilledRequest): Response => {
const newEntry = {request: request.currentRequest, response: request.httpRequest};
return {
accumulatedHttpResponses: [...storage.accumulatedHttpResponses, newEntry],
response: request.httpRequest
}
}
const getIgnoredRequest = (date: number, requests: Requests): Requests => ({
currentRequest: date,
action: Action.IgnoreRequest,
accumulatedRequests: requests.accumulatedRequests
})
const getProcessedRequests = (date: number, requests: Requests): Requests => ({
currentRequest: date,
action: Action.ProcessRequest,
accumulatedRequests: [...requests.accumulatedRequests, date]
})
const processRequest = (requests: Requests, date: number): Requests => {
const requestExists = requests.accumulatedRequests.some(request => request === date);
return requestExists
? getIgnoredRequest(date, requests)
: getProcessedRequests(date, requests)
}
const processFulfilledRequest = (storage: Response, request: FulfilledRequest): Response => isUpdateAction(request.action)
? getResponseForNewRequest(storage, request)
: getResponseForExistingRequest(storage, request)
const fulfillFakeRequest = (requests: Requests): Observable<FulfilledRequest> => of('').pipe(
map(response => ({...requests, httpRequest: response})),
)
const fulfillHttpRequest = (requests: Requests): Observable<FulfilledRequest> => fakeHttp(requests.currentRequest).pipe(
map(response => ({...requests, httpRequest: response}))
)
Final Connection via Observables
const date$: Subject<number> = new Subject();
const response$ = date$.pipe(
scan(processRequest, defaultRequests),
switchMap((requests): Observable<FulfilledRequest> => isUpdateAction(requests.action)
? fulfillHttpRequest(requests)
: fulfillFakeRequest(requests)
),
scan(processFulfilledRequest, defaultResponseStorage),
map(response => response.response)
)

General solution for pre-emptive background work scheduling on javascript

Here is the scenario:
When my web app starts, I want to load data from several tables in local storage (using indexedDB). I delegate this work to a web worker. It will load each table in turn, and fire a message with the data as it loads each one. On the main thread, a listener will receive the message and store the data in a cache.
But let's say the user presses a button to view the data for a specific table. The app calls a function that checks the cache, and sees that the data for that table has not been loaded yet.
How does this function wait until the data for that table has been cached so that it can return the data? Even more important, what if the table is scheduled to be loaded last? How can this function send a message to the web worker to prioritize loading that specific table so that its data will available as soon as possible?
What is a general pattern for a clean solution to this pre-emptive scheduling problem? I would like to avoid polling if at all possible.
The Worker may use an asynchronous queue that contains all the tables to be loaded and is sorted after a certain priority, so you can priorize certain tables and they get sorted to the front of the table. As you havent shown a real implementation here is a more generalized version:
class AsyncPriorityQueue {
constructor(task){
this.task = task;
this.queue = [];
}
push(element, priority = 0){
const pos = this.queue.findIndex(el => el.priority < priority) + 1;
this.queue.splice(pos, 0, {element, priority});
if(this.running) return;
this.running = true;
this._run();
}
prioritize(element, priority = 10){
const pos = this.queue.findIndex(el => el.element === element);
if(pos != -1) this.queue.splice(pos, 1);
this.push(element, priority);
}
async _run(){
while(this.queue.length)
await this.task(this.queue.shift().element);
}
}
Note: If the task is not asynchronous you should use sth like setTimeout(next, 0) to allow the process messaging to interrupt it...
A sample implementation could be an image loader:
class ImageLoader extends AsyncPriorityQueue {
constructor(){
super(function task(url){
const img = new Image();
img.src = url;
return new Promise(res => img.onload = res);
});
}
}
const loader = new ImageLoader;
loader.push("a.jpg");
loader.push("b.jpg", 1); // a bit more important
// Oh, wait:
loader.prioritize("a.jpg");

firefox detect tab id in "sdk/system/events" api

Good day.
i have problem with porting chromium extension to firefox.
i need to detect all outgoing request and id's of tabs to which it belongs.
to detect requests i using system/events api, but i can't find a way how to detect id of tab from incomming events. As i understand this events is xpcom objects and i should use QueryInterface to get some interface to get some other interface to get some other interface to get some other interface ..... to get some other interface to get id of tab from it (just like in COM implementation in windows), but i can't find which interface i need, can't find documentation about this events at all...
code which i using in chromium:
chrome.webRequest.onBeforeRequest.addListener(
function(info) {
if(info.tabId)
//do stuff here
}
so it's what i want to achieve from firefox...
code which i currently write for firefox:
exports.main = function(options)
{
//stuf here ....
........
function listener(event)
{
var channel = event.subject.QueryInterface(Ci.nsIHttpChannel);
console.log(channel);
//TODO: get tab here somehow
}
events.on("http-on-opening-request", listener);
}
i have looked on xpcom docs few days, but still have not enough info to implement this simple thing... so if someone have success with this, please help.
I just found a code snippet for getting the browser that fires the http-on-modify-request notification. The code there seems to be broken but I used some of it to create this function to get a tab from the channel.
const getTabFromChannel = (aChannel) => {
try {
let notificationCallbacks = aChannel.notificationCallbacks || aChannel.loadGroup.notificationCallbacks;
if (!notificationCallbacks)
return null;
let domWin = notificationCallbacks.getInterface(Ci.nsIDOMWindow);
let chromeTab = tabsUtils.getTabForContentWindow(domWin);
return getSdkTabFromChromeTab(chromeTab);
}
catch (e) {
// some type errors happen here, not sure how to handle them
console.log(e);
return null;
}
}
This function converts the low-level tab to a high-level tab. Depending on which one you need you could skip this function of course. Again, in the latest SDK you probably can replace it with tabs.viewFor(chromeTab).
const tabs = require("sdk/tabs");
const tabsUtils = require("sdk/tabs/utils");
const getSdkTabFromChromeTab = (chromeTab) => {
const tabId = tabsUtils.getTabId(chromeTab);
for each (let sdkTab in tabs){
if (sdkTab.id === tabId) {
return sdkTab;
}
}
return null;
};
There seems to be a problem that the listener fails when switching between windows when using system/events. Use Services.obs.addObserver instead:
const httpRequestObserver = {
observe: function (subject, topic, data) {
var channel = subject.QueryInterface(Ci.nsIHttpChannel);
console.log("channel");
var tab = getTabFromChannel(channel);
if(tab) {
console.log("request by tab", tab.id);
}
}
}
exports.main = function() {
Cu.import('resource://gre/modules/Services.jsm');
Services.obs.addObserver(httpRequestObserver, 'http-on-opening-request', false);
}
I can only hope that it works for all the requests you need to detect. The documentation already mentions some cases where it won't work:
Note that some HTTP requests aren't associated with a tab; for example, RSS feed updates, extension manager requests, XHR requests from XPCOM components, etc.
The article Listening to events on all tabs describes how to set up web progress listeners for tabs. With this listener you can get requests and redirects.
const tabsUtils = require("sdk/tabs/utils");
const listener = {
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener", "nsISupportsWeakReference"]),
onLocationChange: (browser, progress, request, uri) => {
let tab = tabsUtils.getTabForContentWindow(progress.DOMWindow);
// ...
},
onStateChange: (browser, progress, request, state) => {
let tab = tabsUtils.getTabForContentWindow(progress.DOMWindow);
// ...
}
// ...
};
getChromeWindow(sdkWindow).getBrowser().addTabsProgressListener(listener);
At some point you may need to convert between low- and high-level tabs or chrome/dom/sdk windows which is implemented really bad and confusing. An sdk window in this case is one you get with windows.browserWindows, the chrome window has a reference to the gBrowser. If you are using the latest sdk maybe this helps: https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_API/tabs#Converting_to_XUL_tabs and https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/windows#Converting_to_DOM_windows. I used this function to get the chrome window from a sdk window: https://bugzilla.mozilla.org/show_bug.cgi?id=695143#c15
const { BrowserWindow } = require('sdk/windows');
const { windows } = require('sdk/window/utils');
function getChromeWindow(sdkWindow) {
// to include private window use the as second argument
// { includePrivate: true }
for (let window of windows('navigator:browser'))
if (BrowserWindow({window: window}) === sdkWindow)
return window;
return null;
}

Javascript library for real-time and offline web app

I have a back-end server with a REST api. On the front-end, I use Angular.js. To handle the real-time part, I would like to use a third-party service such as Pusher.
I'm looking for a simple library that could handle the M of the mvc part of the front-end. More specifically, I would like a Model interface that would abstract away the complexity of the offline and real-time aspects.
For instance, from the Angular side, I would like to subscribe to my Model and get notified when it changes. I would also like to have a .save() method that would handle the syncing with the server and other clients.
That library should:
Work offline: it will save the data in the local_storage and sync back with the server when it gets back online.
Listen to real-time changes, update its model and propagate the changes to the listeners.
Work well with a standard REST interface.
So, just as a quick pseudocode example, in Angular I would like to do:
my_controller = function($scope) {
User.find_all(function(users) {
$scope.users = users;
});
}
User is the model abstraction.. when it gets a real-time update, my $scope.users should change accordingly.
$scope.users[0].set('name', 'testing)
This should save the model to the server. Or, if offline, should save it locally and sync it later on when it's back online.
I know there are online services trying to accomplish that, such as Firebase and kinvey. The problem with these tools is that it only offers a hosted solution. I need to controller the REST server and the database. So, basically, I'm looking for a "Firebase" library - without all the authentications and authorizations - that could work for with a REST server and pubsub third party.
Thanks!
this is a bit long for an answer, but i don't have it published yet.
function monitor(obj, callBack){
var api={
patch: patchObjectWithDiff,
init: init,
resolve: resolve,
snapshot: snapshot,
diff: diff,
update: changeMonitor
};
function merge2(o, ob) {
for (var z in ob) {
if (ob.hasOwnProperty(z)) {
if(typeof ob[z]=="object"){
if(ob[z]==null){
delete o[z];
}else{
merge2( o[z] || {}, ob[z]);
}
}else{
o[z] = ob[z];
}
}
}
return o;
}
function snapshot(obj) {
var out = [];
function merge3(ob, path) {
path = path || [];
var tp;
for(var z in ob) {
if(ob.hasOwnProperty(z)) {
if(ob[z] && typeof ob[z] == "object" && [Date, RegExp].indexOf(ob[z].constructor) == -1) {
tp=path.concat(z);
out.push({
path: tp.join("`"),
path2: tp,
dt: "set",
date: +new Date,
v: Array.isArray(ob[z]) ? "[]" : "{}"
});
merge3(ob[z], path.concat(z));
} else {
tp=path.concat(z);
out.push({
path: tp.join("`"),
path2: tp,
type: "set",
dt: +new Date,
v: JSON.stringify(ob[z])
});
}
}
}
}
merge3(obj);
return out;
};
function diff(d1, d2){
var out=d2.filter(function(a,b,c){
var ov=JSON.stringify(a.v);
return d1.some(function(aa,bb){ return aa.path==a.path && JSON.stringify(aa.v) != ov; });
}),
// find deletions
dels=d1.filter(function(a,b,c){
return !d2.some(function(aa,bb){ if(aa.path==a.path ){ return true; }; });
}),
allPaths=dels.map(function(a){return a.path}).sort(),
dels2=dels.filter(function eliminateUnneededSubBranches(a){
var pos=allPaths.indexOf( a.path2.slice(0,-1).join("`") );
return pos==-1 || pos >= allPaths.indexOf(a.path);
}).map(function(a){a.type="del"; delete a.v; return a;});
[].push.apply(out, dels2);
//find inserts
var outNew=d2.filter(function(a,b,c){
var ov=JSON.stringify(a.v);
return !d1.some(function(aa,bb){ return aa.path==a.path });
});
[].push.apply(out, outNew);
return out.map(function(a){
var x= {
dt: a.dt,
k: a.path2
};
if(a.hasOwnProperty("v")){ x.v=a.v; }
return x;
a.k=a.path2;
delete a.path;
delete a.path2;
delete a.type;
return a;
});
}
function resolve(path, object){
var tob=object;
path.map(function(a){ return (tob=tob[a])||tob; })
return tob;
}
function patchObjectWithDiff(diff, object){
diff.forEach(function(a,b,c){
var p= resolve(a.k.slice(0,-1), object),
k= a.k.slice(-1)[0];
if(a.hasOwnProperty("v")){ //set:
p[k]=JSON.parse(a.v);
if(String(p[k]).match(/Z$/)){ p[k]=new Date(''+p[k]) || p[k]; }
}else{ // del:
if(Array.isArray(p)){ p.splice(k,1); }else{ delete p[k]; }
}
});
return object;
}
var init=snapshot(JSON.parse(JSON.stringify(obj))),
id=Math.random()+ Number(new Date());
var init=snapshot(obj);
function changeMonitor(){
var thisTime=snapshot(obj),
diffs=diff(init, thisTime);
if(diffs.length){
api.diffs=diffs;
(callBack||console.log.bind(console))("objectUpdate", diffs );
init=thisTime;
}//end if change?
}
setInterval(changeMonitor, 2500);
return api;
}
demo / example usage:
var obj={a:1, b:[1,2,3], c: false}; // a model object
var dupe=JSON.parse(JSON.stringify(obj)); // a cheap clone of the data for demo use
//subscribe this object to updates
var mon=monitor(obj, function(type, changes){console.log(type, changes); });
// make some changes to the object:
obj.e="cool!";
obj.b.push(5);
obj.a=7;
// manually call update instead of waiting for the bundler:
// (this is needed for this demo so we can reconcile the changes in sync and view the output)
mon.update();
// now apply stored changes to the clone of the orig data:
var updatedDupe= mon.patch(mon.diffs, dupe);
// use a cheap and easy but not production-reliable to compare the objects:
JSON.stringify(updatedDupe)==JSON.stringify(obj); // should be true
tested in chrome and firefox.
be aware that this particular demo's use of JSON depends on some luck, and consistent key ordering, which is not guaranteed by the JS spec. Key order doesn't really matter, but it might cause the JSON.stringify() == comparison to fail, even though the object's properties are indeed sync'd. This is just for demonstration's sake to get a true/false answer if it works, don't beat me up...
you can give it a custom callback to send("diff", {diffs:mon.diffs}) the changes as they happen and then use a subscribed event from pusher et al like on("diff", function(e){mon.patch(e.diffs, obj);}); to apply your changes and trigger the view update in your MVC.
I'll leave it to you to work localStorage and online/offline in there as you need, it should be really easy after getting this far.
All diffs in the change list come with three keys:
{"dt":1392348959730,"k":["b","3"],"v":"5"}
dt: a timestamp of when the change was discovered
k: the key path where the change was detected
v: what the discovered changed value is as of dt
This script is hot off the press and i haven't had time to write proper documentation, but i figure it might help or at least inspire a solution that works for you.
I think you should start researching HTML5 WebSockets: http://www.websocket.org/
It allows bi-directional communication between server and client, client pull and server push.
Then look at SignalR, the asp.net implementation of HTML5 WebSockets: http://www.asp.net/signalr

Categories