Since the release of iOS6, my web app has hit a series of bugs, one of the worst being what I'm almost 100% positive is websql transactions being queued. When I first load the app in mobile safari( ipad ), the transactions work fine. Then, if I close safari and open it again the transactions seem to be queued and never execute.
If I open the dev tools and run a simple alert, the methods will fire, if I just hit reload the transactions work fine as well, or if I delay the running of the db transactions by 1sec or something it works fine as well.
I do not want to run a setTimeout to run the transactions.
Is this a caching issue that safari has now since implemented?
If anyone has ANY good ideas on how to fix this please answer below.
Thanks in advance.
It may not be bug. You may be using series of transaction unnecessarily. You could use mulitple requests per transaction. onsuccess callback, you can reuse the transaction. It should work. At the same time, limit number of requests per transaction. setTimeout should never necessary.
Here is how a single transaction is used to insert multiple objects
/**
* #param {goog.async.Deferred} df
* #param {string} store_name table name.
* #param {!Array.<!Object>} objects object to put.
* #param {!Array.<(Array|string|number)>=} opt_keys
*/
ydn.db.req.WebSql.prototype.putObjects = function (df, store_name, objects, opt_keys) {
var table = this.schema.getStore(store_name);
if (!table) {
throw new ydn.db.NotFoundError(store_name);
}
var me = this;
var result_keys = [];
var result_count = 0;
/**
* Put and item at i. This ydn.db.core.Storage will invoke callback to df if all objects
* have been put, otherwise recursive call to itself at next i+1 item.
* #param {number} i
* #param {SQLTransaction} tx
*/
var put = function (i, tx) {
// todo: handle undefined or null object
var out;
if (goog.isDef(opt_keys)) {
out = table.getIndexedValues(objects[i], opt_keys[i]);
} else {
out = table.getIndexedValues(objects[i]);
}
//console.log([obj, JSON.stringify(obj)]);
var sql = 'INSERT OR REPLACE INTO ' + table.getQuotedName() +
' (' + out.columns.join(', ') + ') ' +
'VALUES (' + out.slots.join(', ') + ');';
/**
* #param {SQLTransaction} transaction transaction.
* #param {SQLResultSet} results results.
*/
var success_callback = function (transaction, results) {
result_count++;
result_keys[i] = goog.isDef(out.key) ? out.key : results.insertId;
if (result_count == objects.length) {
df.callback(result_keys);
} else {
var next = i + ydn.db.req.WebSql.RW_REQ_PER_TX;
if (next < objects.length) {
put(next, transaction);
}
}
};
/**
* #param {SQLTransaction} tr transaction.
* #param {SQLError} error error.
*/
var error_callback = function (tr, error) {
if (ydn.db.req.WebSql.DEBUG) {
window.console.log([sql, out, tr, error]);
}
df.errback(error);
return true; // roll back
};
//console.log([sql, out.values]);
tx.executeSql(sql, out.values, success_callback, error_callback);
};
if (objects.length > 0) {
// send parallel requests
for (var i = 0; i < ydn.db.req.WebSql.RW_REQ_PER_TX && i < objects.length; i++) {
put(i, this.getTx());
}
} else {
df.callback([]);
}
};
Regarding transaction queue, it is better to handle by the application rather than by the SQLite for robustness. Basically we can watch transaction complete event before starting a new transaction. It is also fine to run multiple transactions as long as they are under control. Out of control will be opening transactions under a loop. Generally I will open only couple of transactions.
Here is how transaction is queued:
/**
* Create a new isolated transaction. After creating a transaction, use
* {#link #getTx} to received an active transaction. If transaction is not
* active, it return null. In this case a new transaction must re-create.
* #export
* #param {Function} trFn function that invoke in the transaction.
* #param {!Array.<string>} store_names list of keys or
* store name involved in the transaction.
* #param {ydn.db.TransactionMode=} opt_mode mode, default to 'readonly'.
* #param {function(ydn.db.TransactionEventTypes, *)=} oncompleted
* #param {...} opt_args
* #override
*/
ydn.db.tr.TxStorage.prototype.transaction = function (trFn, store_names, opt_mode, oncompleted, opt_args) {
//console.log('tr starting ' + trFn.name);
var scope_name = trFn.name || '';
var names = store_names;
if (goog.isString(store_names)) {
names = [store_names];
} else if (!goog.isArray(store_names) ||
(store_names.length > 0 && !goog.isString(store_names[0]))) {
throw new ydn.error.ArgumentException("storeNames");
}
var mode = goog.isDef(opt_mode) ? opt_mode : ydn.db.TransactionMode.READ_ONLY;
var outFn = trFn;
if (arguments.length > 4) { // handle optional parameters
var args = Array.prototype.slice.call(arguments, 4);
outFn = function () {
// Prepend the bound arguments to the current arguments.
var newArgs = Array.prototype.slice.call(arguments);
//newArgs.unshift.apply(newArgs, args); // pre-apply
newArgs = newArgs.concat(args); // post-apply
return trFn.apply(this, newArgs);
}
}
outFn.name = scope_name;
var me = this;
if (this.mu_tx_.isActive()) {
//console.log(this + ' active')
this.pushTxQueue(outFn, store_names, mode, oncompleted);
} else {
//console.log(this + ' not active')
var transaction_process = function (tx) {
me.mu_tx_.up(tx, scope_name);
// now execute transaction process
outFn(me);
me.mu_tx_.out(); // flag transaction callback scope is over.
// transaction is still active and use in followup request handlers
};
var completed_handler = function (type, event) {
me.mu_tx_.down(type, event);
/**
* #preserve_try
*/
try {
if (goog.isFunction(oncompleted)) {
oncompleted(type, event);
}
} catch (e) {
// swallow error. document it publicly.
// this is necessary to continue transaction queue
if (goog.DEBUG) {
throw e;
}
} finally {
me.popTxQueue_();
}
};
if (ydn.db.tr.TxStorage.DEBUG) {
window.console.log(this + ' transaction ' + mode + ' open for ' + JSON.stringify(names) + ' in ' + scope_name);
}
this.storage_.newTransaction(transaction_process, names, mode, completed_handler);
}
};
As it turns out, initializing Facebook before websql was causing the problem. After commenting out FB the app behaved properly, which is why setTimeout solved the issue as well; the fb api was ready. How the thread of execution gets blocked, I don't know.
So, to anyone using FB and then trying to execute websql transactions...delay FB!
Though, websql is still running a bit slow on safari load...
Related
Hey so im making a leaderboard for a discord bot using discord.js And I want to display users by their names instead of their ID's so using discord.js I use the function .fetchUser(ID)
.fetchUser(ID) is a promise which can take a some of time depending on the bandwidth.
So because discord.js uses a promise my code is no longer Async, I thought that by putting the code in a promise it would run Async.
And I was wrong.
my code:
//This is ran inside a .prototype function so (this) is defined
return new Promise((resolve, reject) => {
this.list = [];
//users is an object with user's IDs as the key
//Currently it only has one key in it (mine)
for (let i in users) {
let pos = 0;
let score = this.getScore(users[i]);
if (score === 0) {
client.fetchUser(i).then((user)=> {
console.log(`pushed`);//logs way after the "finish" is logged
this.list.push([user.username.substring(0,13), score])
});
continue;
}
for (let h = 0; h < this.list.length; h++) {
if (score >= this.list[h][1]) {
pos = h;
break;
}
}
client.fetchUser(users[i].id).then((user) => {
this.list.splice(pos, 0, [user.username.substring(0,13), score])
})
}
console.log(`Finished: `+this.list.length);
resolve(this.list);
})
You have to chain off of Promises you receive. Client#fetchUser() returns a Promise which you are waiting on, but not enough. You have to propagate up Promises. If something in your function call chain is asynchronous, you should consider the whole chain async.
You fill this.list from within the fetchUser(...).then(...), which isn't necessarily bad, as long as you don't try to use list until after fetchUser's resolution chain is done. You aren't doing that; you immediately resolve(this.list).
Consider this abbreviated form of your original function:
return new Promise((resolve, reject) => {
this.list = [];
for (let i in users) {
// A promise is created right here
client.fetchUser(i).then((user) => {
// This will populate list AFTER the then callback
this.list.push([user.username.substring(0, 13), score])
});
}
// You aren't waiting until the promise created by fetchUser completes
resolve(this.list);
})
this.list can't be considered "complete" until all the users involved have had their profiles loaded and their scores retrieved. Considering that, we can use Promise.all() which takes an array of Promises and then resolves once all of the provided promises have resolved. So to wait that way, we would do something like this, which still isn't ideal, but waits correctly:
return new Promise((resolve, reject) => {
this.list = [];
// This is an array of Promises
const discordUsersPromise = users.map(user => client.fetchUser(user));
// Wait till all the fetchUser calls are done
const listIsPopulatedPromise = Promise.all(discordUsersPromise).then(dUsers => {
// This replaces your for (let i in users) {}
Object.entries(users).forEach((user, idx) => {
const score = this.getScore(user);
const discordUser = dUsers[idx];
this.list.push([discordUser.username.substring(0, 13), score])
});
});
// We still have to wait for the list to be completely populated
return listIsPopulatedPromise.then(() => this.list);
})
Consider this implementation. I have made some assumptions about your code since you use this.list but don't include what this is an instance of, but most of it should be the same:
/**
* Object to composite certain user properties
* #typedef {RealUser}
* #property {String} user The local string for the user
* #property {User} realUser The user that Discord gives us
* #property {Number} score The score this user has
*/
/**
* Class to encapsulate user and score and data
*/
class Game {
/**
* Constructs a game
*/
constructor() {
/**
* The users we are keeping score of
* #type {Object}
*/
this.users = {};
}
/**
* Get the score of a particular user
* #param {String} user User to get score of
* #returns {Number} User's score
*/
getScore(user) {
return this.users[user] || 0;
}
/**
* Get a composite of users and their status
* #param {String[]} users The users to put on our leaderboard
* #returns {Promise<RealUser[]>} Sorted list of users that we included in our leaderboard
*/
getLeaderBoard(users) {
// Map all the users that we are given to Promises returned bye fetchUser()
const allRealUsersPromise = Promise.all(users.map(user => client.fetchUser(user)
/*
* Create an object that will composite the string that we use
* to note the user locally, the Discord User Object, and the
* current score of the user that we are tracking locally
*/
.then(realUser => ({
user,
realUser,
score: this.getScore(user)
}))));
/*
* Once we have all the data we need to construct a leaderboard,
* we should sort the users by score, and hand back an array
* of RealUsers which should contain all the data we want to
* print a leaderboard
*/
return allRealUsersPromise
.then(scoredUsers => scoredUsers.sort((a, b) => a.score - b.score));
}
/**
* Prints out a leaderboard
* #param {String[]} users The users to include on our leaderboard
*/
printLeaderBoard(users) {
// Go get a leaderboard to print
this.getLeaderBoard(users).then(sortedScoredUsers => {
// Iterate our RealUsers
sortedScoredUsers.forEach((sortedScoredUser, idx) => {
const username = sortedScoredUser.realUser.username;
const score = sortedScoredUser.score;
// Print out their status
console.log(`${username.substring(0, 13)} is in position ${idx + 1} with ${score} points`);
});
});
}
}
const game = new Game();
game.users["bob"] = 5;
game.users["sue"] = 7;
game.users["tim"] = 3;
game.printLeaderBoard(Object.keys(game.users));
I have following code which process a queue and I need to exist the function when there are no messages in the queue and there is no enough time to process more messages. My problem is, it doesn't jump out of the function upon failing the condition and I think it's due to that this a recursive function but I cannot figure it out.
/**
* Check if there is enough time to process more messages
*
* #param {} context
* #returns {boolean}
*/
async function enoughTimeToProcess(context) {
return context.getRemainingTimeInMillis() > 230000;
}
/**
* Consume the queue and increment usages
*
* #param context
*
* #returns {boolean}
*/
async function process(context) {
const messagesPerRequest = queueConst.messagesPerRequest;
const messagesToBeDeleted = [];
const queue = new queueClient();
const messages = await queue.getMessages(messagesPerRequest);
if (messages === undefined) {
if (await enoughTimeToProcess(context) === true) {
await process(context);
} else {
return false;
}
}
const responses = messages.map(async(messageItem) => {
const messageBody = JSON.parse(messageItem.Body);
const parsedMessage = JSON.parse(messageBody.Message);
const accountId = parsedMessage[0].context.accountId;
let code = parsedMessage[0].context.code;
// Our DB support only lowercase characters in the path
code = code.toLowerCase();
const service = parsedMessage[0].name;
const count = parsedMessage[0].increment;
const storageResponse = await incrementUsage(
{ storageClient: storage, code, accountId, service, count }
);
if (storageResponse) {
messagesToBeDeleted.push({
Id: messageItem.MessageId,
ReceiptHandle: messageItem.ReceiptHandle,
});
}
return 1;
});
const processedMessages = await Promise.all(responses);
const processedMessagesCount = processedMessages.length;
if (messagesToBeDeleted.length > 0) {
console.log(`${processedMessagesCount} messages processed.`);
await queue.deleteMessageBatch(messagesToBeDeleted);
}
if (await enoughTimeToProcess(context) === true) {
await process(context);
}
return true;
}
I think the problem can be when messages are undefined and there is still enough time, because the recursive function is going to be called infinite times, because it always accomplishes both conditions, and probably it exceeds the available resources.
Try to sleep some time before calling process function again, just to be sure it is the problem
I'm trying to build a small wrapper library for the node redis module.
var redis = require('redis'),
client = redis.createClient();
module.exports = {
/*
* Time To Live
*
* Time in seconds the cache will remain in
* memory.
*/
ttl: 120,
/*
* Time To Refresh
*
* Time buffer in seconds when the cache should
* be refreshed. On a cache check, if the ttl
* is less than the ttr, the cache will be
* refreshed.
*/
ttr: 60,
/*
* Check Cache
*
* Middleware to check if the request is cached
* before executing the full request.
*/
check: function (req, res, next) {
var key = req.url.slice(1, req.url.length).replace('/', ':');
client.get(key, function (err, value) {
if (value !== null) {
res.send(value);
client.ttl(key, function (err, ttl) {
if (ttl < this.ttr) {
return next();
}
return;
});
} else {
return next();
}
})
},
/*
* Set Cache
*
* Takes a key and a value and stores it in redis.
* Also takes the full response object "res" and
* handles sending the response if it has not
* already been sent.
*/
set: function (url, value, res) {
var key = url.slice(1, url.length).replace('/', ':');
client.set(key, value);
client.expire(key, this.ttl);
if (!res.headersSent) {
res.send(value);
}
return;
},
/*
* Keygen Cache
*
* Takes a urls substring and creates a keyname
* in line with redis best practices
*/
keygen: function (url) {
var key = url.slice(0,1).replace('/', ':');
console.log(key);
return key;
}
};
I can't figure out how to use the 'this' keyword properly. If I try to reference this.ttl or this.ttr or call this.keygen from another function within the module.exports object it always turns up undefined. What is a structure I should be using to enable referencing functions internal to the same object?
You have nested functions in your check function, so the this-keyword relates to the inner function context and not to your object.
First solution:
function (req, res, next) {
var _this = this;
var key = req.url.slice(1, req.url.length).replace('/', ':');
client.get(key, function (err, value) {
if (value !== null) {
res.send(value);
client.ttl(key, function (err, ttl) {
if (ttl < _this.ttr) {
return next();
}
return;
});
} else {
return next();
}
})
}
And if you are using the function as a callback function bind your object to the callback function like this:
// replace "yourModule" with the name of your module
var yourModule = require('./yourModule.js');
// an example for expressjs (use get, post ... or something else)
app.get('/some/url', yourModule.check.bind(yourModule));
Second solution (IMHO the better one):
Another solution is to use local variables in your module (these are only visible in your module -> and this is what you want).
This is way more easier:
/*
* Time To Live
*
* Time in seconds the cache will remain in
* memory.
*/
var ttl = 120;
/*
* Time To Refresh
*
* Time buffer in seconds when the cache should
* be refreshed. On a cache check, if the ttl
* is less than the ttr, the cache will be
* refreshed.
*/
var ttr = 60;
/*
* Check Cache
*
* Middleware to check if the request is cached
* before executing the full request.
*/
var check = function (req, res, next) {
var key = req.url.slice(1, req.url.length).replace('/', ':');
client.get(key, function (err, value) {
if (value !== null) {
res.send(value);
client.ttl(key, function (err, ttl) {
if (ttl < ttr) {
return next();
}
return;
});
} else {
return next();
}
})
};
/*
* Set Cache
*
* Takes a key and a value and stores it in redis.
* Also takes the full response object "res" and
* handles sending the response if it has not
* already been sent.
*/
var set = function (url, value, res) {
var key = url.slice(1, url.length).replace('/', ':');
client.set(key, value);
client.expire(key, ttl);
if (!res.headersSent) {
res.send(value);
}
return;
};
/*
* Keygen Cache
*
* Takes a urls substring and creates a keyname
* in line with redis best practices
*/
var keygen = function (url) {
var key = url.slice(0,1).replace('/', ':');
console.log(key);
return key;
};
/**
* PUBLIC API
*/
module.exports = {
check: check,
set: set,
keygen: keygen
};
Now because you are not relying on the context of the module you can simply do this:
// replace "yourModule" with the name of your module
var yourModule = require('./yourModule.js');
// an example for expressjs (use get, post ... or something else)
app.get('/some/url', yourModule.check);
I have the following code "js", and the browser puts this error "in line 8, "undefined is not a function".
The line 8 is: $('.wysiwyg', context).once('wysiwyg', function () {
I have no idea about what happens!
Any suggestions? Thank you.
Drupal.behaviors.attachWysiwyg = {
attach: function (context, settings) {
// This breaks in Konqueror. Prevent it from running.
if (/KDE/.test(navigator.vendor)) {
return;
}
$('.wysiwyg', context).once('wysiwyg', function () {
if (!this.id || typeof Drupal.settings.wysiwyg.triggers[this.id] === 'undefined') {
return;
}
var $this = $(this);
var params = Drupal.settings.wysiwyg.triggers[this.id];
for (var format in params) {
params[format].format = format;
params[format].trigger = this.id;
params[format].field = params.field;
}
var format = 'format' + this.value;
// Directly attach this editor, if the input format is enabled or there is
// only one input format at all.
if ($this.is(':input')) {
Drupal.wysiwygAttach(context, params[format]);
}
// Attach onChange handlers to input format selector elements.
if ($this.is('select')) {
$this.change(function() {
// If not disabled, detach the current and attach a new editor.
Drupal.wysiwygDetach(context, params[format]);
format = 'format' + this.value;
Drupal.wysiwygAttach(context, params[format]);
});
}
// Detach any editor when the containing form is submitted.
$('#' + params.field).parents('form').submit(function (event) {
// Do not detach if the event was cancelled.
if (event.isDefaultPrevented()) {
return;
}
Drupal.wysiwygDetach(context, params[format], 'serialize');
});
});
},
detach: function (context, settings, trigger) {
var wysiwygs;
// The 'serialize' trigger indicates that we should simply update the
// underlying element with the new text, without destroying the editor.
if (trigger == 'serialize') {
// Removing the wysiwyg-processed class guarantees that the editor will
// be reattached. Only do this if we're planning to destroy the editor.
wysiwygs = $('.wysiwyg-processed', context);
}
else {
wysiwygs = $('.wysiwyg', context).removeOnce('wysiwyg');
}
wysiwygs.each(function () {
var params = Drupal.settings.wysiwyg.triggers[this.id];
Drupal.wysiwygDetach(context, params, trigger);
});
}
};
/**
* Attach an editor to a target element.
*
* This tests whether the passed in editor implements the attach hook and
* invokes it if available. Editor profile settings are cloned first, so they
* cannot be overridden. After attaching the editor, the toggle link is shown
* again, except in case we are attaching no editor.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
*/
Drupal.wysiwygAttach = function(context, params) {
if (typeof Drupal.wysiwyg.editor.attach[params.editor] == 'function') {
// (Re-)initialize field instance.
Drupal.wysiwyg.instances[params.field] = {};
// Provide all input format parameters to editor instance.
jQuery.extend(Drupal.wysiwyg.instances[params.field], params);
// Provide editor callbacks for plugins, if available.
if (typeof Drupal.wysiwyg.editor.instance[params.editor] == 'object') {
jQuery.extend(Drupal.wysiwyg.instances[params.field], Drupal.wysiwyg.editor.instance[params.editor]);
}
// Store this field id, so (external) plugins can use it.
// #todo Wrong point in time. Probably can only supported by editors which
// support an onFocus() or similar event.
Drupal.wysiwyg.activeId = params.field;
// Attach or update toggle link, if enabled.
if (params.toggle) {
Drupal.wysiwygAttachToggleLink(context, params);
}
// Otherwise, ensure that toggle link is hidden.
else {
$('#wysiwyg-toggle-' + params.field).hide();
}
// Attach editor, if enabled by default or last state was enabled.
if (params.status) {
Drupal.wysiwyg.editor.attach[params.editor](context, params, (Drupal.settings.wysiwyg.configs[params.editor] ? jQuery.extend(true, {}, Drupal.settings.wysiwyg.configs[params.editor][params.format]) : {}));
}
// Otherwise, attach default behaviors.
else {
Drupal.wysiwyg.editor.attach.none(context, params);
Drupal.wysiwyg.instances[params.field].editor = 'none';
}
}
};
/**
* Detach all editors from a target element.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
* #param trigger
* A string describing what is causing the editor to be detached.
*
* #see Drupal.detachBehaviors
*/
Drupal.wysiwygDetach = function (context, params, trigger) {
// Do not attempt to detach an unknown editor instance (Ajax).
if (typeof Drupal.wysiwyg.instances[params.field] == 'undefined') {
return;
}
trigger = trigger || 'unload';
var editor = Drupal.wysiwyg.instances[params.field].editor;
if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) {
Drupal.wysiwyg.editor.detach[editor](context, params, trigger);
}
};
/**
* Append or update an editor toggle link to a target element.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
*/
Drupal.wysiwygAttachToggleLink = function(context, params) {
if (!$('#wysiwyg-toggle-' + params.field).length) {
var text = document.createTextNode(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable);
var a = document.createElement('a');
$(a).attr({ id: 'wysiwyg-toggle-' + params.field, href: 'javascript:void(0);' }).append(text);
var div = document.createElement('div');
$(div).addClass('wysiwyg-toggle-wrapper').append(a);
$('#' + params.field).after(div);
}
$('#wysiwyg-toggle-' + params.field)
.html(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable).show()
.unbind('click.wysiwyg', Drupal.wysiwyg.toggleWysiwyg)
.bind('click.wysiwyg', { params: params, context: context }, Drupal.wysiwyg.toggleWysiwyg);
// Hide toggle link in case no editor is attached.
if (params.editor == 'none') {
$('#wysiwyg-toggle-' + params.field).hide();
}
};
/**
* Callback for the Enable/Disable rich editor link.
*/
Drupal.wysiwyg.toggleWysiwyg = function (event) {
var context = event.data.context;
var params = event.data.params;
if (params.status) {
// Detach current editor.
params.status = false;
Drupal.wysiwygDetach(context, params);
// After disabling the editor, re-attach default behaviors.
// #todo We HAVE TO invoke Drupal.wysiwygAttach() here.
Drupal.wysiwyg.editor.attach.none(context, params);
Drupal.wysiwyg.instances[params.field] = Drupal.wysiwyg.editor.instance.none;
Drupal.wysiwyg.instances[params.field].editor = 'none';
Drupal.wysiwyg.instances[params.field].field = params.field;
$(this).html(Drupal.settings.wysiwyg.enable).blur();
}
else {
// Before enabling the editor, detach default behaviors.
Drupal.wysiwyg.editor.detach.none(context, params);
// Attach new editor using parameters of the currently selected input format.
params = Drupal.settings.wysiwyg.triggers[params.trigger]['format' + $('#' + params.trigger).val()];
params.status = true;
Drupal.wysiwygAttach(context, params);
$(this).html(Drupal.settings.wysiwyg.disable).blur();
}
}
/**
* Parse the CSS classes of an input format DOM element into parameters.
*
* Syntax for CSS classes is "wysiwyg-name-value".
*
* #param element
* An input format DOM element containing CSS classes to parse.
* #param params
* (optional) An object containing input format parameters to update.
*/
Drupal.wysiwyg.getParams = function(element, params) {
var classes = element.className.split(' ');
var params = params || {};
for (var i = 0; i < classes.length; i++) {
if (classes[i].substr(0, 8) == 'wysiwyg-') {
var parts = classes[i].split('-');
var value = parts.slice(2).join('-');
params[parts[1]] = value;
}
}
// Convert format id into string.
params.format = 'format' + params.format;
// Convert numeric values.
params.status = parseInt(params.status, 10);
params.toggle = parseInt(params.toggle, 10);
params.resizable = parseInt(params.resizable, 10);
return params;
};
/**
* Allow certain editor libraries to initialize before the DOM is loaded.
*/
Drupal.wysiwygInit();
// Respond to CTools detach behaviors event.
$(document).bind('CToolsDetachBehaviors', function(event, context) {
Drupal.behaviors.attachWysiwyg.detach(context, {}, 'unload');
});
})(jQuery);
Sounds like you might be missing a jquery file defining the "once" function. Check with Firebug for Firefox (or a similiar development tool) where you can see the requested files for each page - clear the browser cache, then check Firebug's Net tab when reloading the page. If any files are not listed with a HTTP status code of "200 OK", you'll need to check that the files exist and have read permissions so the server can hand them out.
Anyway try to change line 8 as:
$('.wysiwyg:not(.processed)', context).addClass('processed').each(function() {
In my project, I'm trying to use HTML5 appcache to cache static resources like CSS and JS, and "user specific" files such as images and videos. When I say user specific images/videos, I'm trying to have separate files for each user and I need to control order of the file download as well.
Given the scenario, my manifest file will be dynamically loaded for every user. Is there a way where I can get a list of resources that are already cached in client side?
If not, is is possible to read the ".appcache" file in client?
Yes. You can use AJAX request to get the manifest cache file and then read it.
However, this does not guarantee that the browser in the question has the files available.
Below is an sample code
Which checks if we have cached HTML5 app or not
If we are not in a cached state then count loaded resources in the manifest and display a progress bar according to the manifest cache entry count (total) and do a manual AJAX GET request for all URLs to warm up the cache. The browser will do this itself, but this way we can get some progress information out of the process.
When cache is in a known good state, move forward
Disclaimer: not tested to work since 2010
/**
* HTML5 offline manifest preloader.
*
* Load all manifest cached entries, so that they are immediately available during the web app execution.
* Display some nice JQuery progress while loading.
*
* #copyright 2010 mFabrik Research Oy
*
* #author Mikko Ohtamaa, http://opensourcehacker.com
*/
/**
* Preloader class constructor.
*
* Manifest is retrieved via HTTP GET and parsed.
* All cache entries are loaded using HTTP GET.
*
* Local storage attribute "preloaded" is used to check whether loading needs to be performed,
* as it is quite taxing operation.
*
* To debug this code and force retrieving of all manifest URLs, add reloaded=true HTTP GET query parameter:
*
*
*
* #param {Function} endCallback will be called when all offline entries are loaded
*
* #param {Object} progressMonitor ProgressMonitor object for which the status of the loading is reported.
*/
function Preloader(endCallback, progressMonitor, debug) {
if(!progressMonitor) {
throw "progressMonitor must be defined";
}
this.endCallback = endCallback;
this.progressMonitor = progressMonitor;
this.logging = debug; // Flag to control console.log() output
}
Preloader.prototype = {
/**
* Load HTML5 manifest and parse its data
*
* #param data: String, manifest file data
* #return Array of cache entries
*
* #throw: Exception if parsing fails
*/
parseManifest : function(data) {
/* Declare some helper string functions
*
* http://rickyrosario.com/blog/javascript-startswith-and-endswith-implementation-for-strings/
*
*/
function startswith(str, prefix) {
return str.indexOf(prefix) === 0;
}
var entries = [];
var sections = ["NETWORK", "CACHE", "FALLBACK"];
var currentSection = "CACHE";
var lines = data.split(/\r\n|\r|\n/);
var i;
if(lines.length <= 1) {
throw "Manifest does not contain text lines";
}
var firstLine = lines[0];
if(!(startswith(firstLine, "CACHE MANIFEST"))) {
throw "Invalid cache manifest header:" + firstLine;
}
for(i=1; i<lines.length; i++) {
var line = lines[i];
this.debug("Parsing line:" + line);
// If whitespace trimmed line is empty, skip it
line = jQuery.trim(line);
if(line == "") {
continue;
}
if(line[0] == "#") {
// skip comment;
continue;
}
// Test for a new section
var s = 0;
var sectionDetected = false;
for(s=0; s<sections.length; s++) {
var section = sections[s];
if(startswith(line, section + ":")) {
currentSection = section;
sectionDetected = true;
}
}
if(sectionDetected) {
continue;
}
// Otherwise assume we can check for cached url
if(currentSection == "CACHE") {
entries.push(line);
}
}
return entries;
},
/**
* Manifest is given as an <html> attribute.
*/
extractManifestURL : function() {
var url = $("html").attr("manifest");
if(url === null) {
alert("Preloader cannot find manifest URL from <html> tag");
return null;
}
return url;
},
isPreloaded : function() {
// May be null or false
return localStorage.getItem("preloaded") == true;
},
setPreloaded : function(status) {
localStorage.setItem("preloaded", status);
},
/**
* Check whether we need to purge offline cache.
*
*/
isForcedReload : function() {
// http://www.netlobo.com/url_query_string_javascript.html
function getQueryParam(name) {
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regexS = "[\\?&]"+name+"=([^&#]*)";
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if (results == null) {
return "";
} else {
return results[1];
}
}
if(getQueryParam("reload") == "true") {
return true;
}
return false;
},
/**
* Do everything necessary to set-up offline application
*/
load : function() {
this.debug("Entering preloader");
if (window.applicationCache) {
this.debug("ApplicationCache status " + window.applicationCache.status);
this.debug("Please see http://www.w3.org/TR/html5/offline.html#applicationcache");
} else {
this.silentError("The browser does not support HTML5 applicationCache object");
return;
}
var cold;
if(this.isPreloaded()) {
// We have succesfully completed preloading before
// ...move forward
forceReload = this.isForcedReload();
if (forceReload == true) {
applicationCache.update();
} else {
this.endCallback();
return;
}
cold = false;
} else {
cold = true;
}
var url = this.extractManifestURL();
if(url === null) {
return;
}
this.progressMonitor.startProgress(cold);
$.get(url, {}, jQuery.proxy(manifestLoadedCallback, this));
function manifestLoadedCallback(data, textStatus, xhr) {
this.debug("Manifest retrieved");
var text = data;
manifestEntries = this.parseManifest(text);
this.debug("Parsed manifest entries:" + manifestEntries.length);
this.populateCache(manifestEntries);
}
},
/**
* Bootstrap async loading of cache entries.
*
* #param {Object} entrires
*/
populateCache : function(entries) {
this.manifestEntries = entries;
this.currentEntry = 0;
this.maxEntry = entries.length;
this.loadNextEntry();
},
/**
* Make AJAX request to next entry and update progress bar.
*
*/
loadNextEntry : function() {
if(this.currentEntry >= this.maxEntry) {
this.setPreloaded(true);
this.progressMonitor.endProgress();
this.endCallback();
}
var entryURL = this.manifestEntries[this.currentEntry];
this.debug("Loading entry: " + entryURL);
function done() {
this.currentEntry++;
this.progressMonitor.updateProgress(this.currentEntry, this.maxEntries);
this.loadNextEntry();
}
this.debug("Preloader fetching:" + entryURL + " (" + this.currentEntry + " / " + this.maxEntry + ")");
$.get(entryURL, {}, jQuery.proxy(done, this));
},
/**
* Write to debug console
*
* #param {String} msg
*/
debug : function(msg) {
if(this.logging) {
console.log(msg);
}
},
/**
* Non-end user visible error message
*
* #param {Object} msg
*/
silentError : function(msg) {
console.log(msg);
}
};
function ProgressMonitor() {
}
ProgressMonitor.prototype = {
/**
* Start progress bar... initialize as 0 / 0
*/
startProgress : function(coldVirgin) {
$("#web-app-loading-progress-monitor").show();
if(coldVirgin) {
$("#web-app-loading-progress-monitor .first-time").show();
}
},
endProgress : function() {
},
updateProgress : function(currentEntry, maxEntries) {
}
};
I have also been working on a solution for discovering which file is being cached, and have come up with the following.
.htaccess wrapper for the directory we are grabbing files to appcache.
#.htaccess
<FilesMatch "\.(mp4|mpg|MPG|m4a|wav|WAV|jpg|JPG|bmp|BMP|png|PNG|gif|GIF)$">
SetHandler autho
</FilesMatch>
Action autho /www/restricted_access/auth.php
then my auth.php file returns the file (in chunks) to the browser, but also logs at the same time to the server (I use a DB table) with an earlier declared APPID.
That way while the 'progress' event is detected, an AJAX call can be made to retrieve the last entry for APPID, which contains the file name and how much data has been sent.
The advantage of using this method is that its transparent to other methods accessing the files in the '.htaccess wrapped' folder, and in my case also includes authentication.
When not authorized to access a file for whatever reason I return 'Not Authorized' headers.