mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Allow arbitrary JSON objects to be stored in Session (but not compared with equals). Test Session. Fixes #215.
This commit is contained in:
@@ -858,7 +858,7 @@ Example:
|
||||
|
||||
{{> api_box equals}}
|
||||
|
||||
These two expressions do the same thing:
|
||||
If value is a scalar, then these two expressions do the same thing:
|
||||
|
||||
(1) Session.get("key") === value
|
||||
(2) Session.equals("key", value)
|
||||
@@ -905,6 +905,10 @@ Example:
|
||||
// If Session.get had been used instead of Session.equals, then
|
||||
// when the selection changed, all the items would be re-rendered.
|
||||
|
||||
For object and array session values, you cannot use `Session.equals`; instead,
|
||||
you need to use the `underscore` package and write
|
||||
`_.isEqual(Session.get(key), value)`.
|
||||
|
||||
<h2 id="templates_api"><span>Templates</span></h2>
|
||||
|
||||
A template that you declare as `<{{! }}template name="foo"> ... </{{!
|
||||
|
||||
@@ -726,7 +726,7 @@ Template.api.set = {
|
||||
type: "String",
|
||||
descr: "The key to set, eg, `selectedItem`"},
|
||||
{name: "value",
|
||||
type: "String, Number, Boolean, null, or undefined",
|
||||
type: "JSON-able object or undefined",
|
||||
descr: "The new value for `key`"}
|
||||
]
|
||||
};
|
||||
@@ -735,7 +735,7 @@ Template.api.get = {
|
||||
id: "session_get",
|
||||
name: "Session.get(key)",
|
||||
locus: "Client",
|
||||
descr: ["Get the value of a session variable. If inside a [`Meteor.deps`](#meteor_deps) context, invalidate the context the next time the value of the variable is changed by [`Session.set`](#session_set)."],
|
||||
descr: ["Get the value of a session variable. If inside a [`Meteor.deps`](#meteor_deps) context, invalidate the context the next time the value of the variable is changed by [`Session.set`](#session_set). This returns a clone of the session value, so if it's an object or an array, mutating the returned value has no effect on the value stored in the session."],
|
||||
args: [
|
||||
{name: "key",
|
||||
type: "String",
|
||||
|
||||
@@ -20,3 +20,9 @@ Package.on_use(function (api, where) {
|
||||
|
||||
api.add_files('session.js', where);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('session', 'client');
|
||||
api.add_files('session_tests.js', 'client');
|
||||
});
|
||||
|
||||
@@ -1,93 +1,117 @@
|
||||
// XXX could use some tests
|
||||
(function () {
|
||||
|
||||
Session = _.extend({}, {
|
||||
keys: {}, // key -> value
|
||||
keyDeps: {}, // key -> _ContextSet
|
||||
keyValueDeps: {}, // key -> value -> _ContextSet
|
||||
// XXX come up with a serialization method which canonicalizes object key
|
||||
// order, which would allow us to use objects as values for equals.
|
||||
var stringify = function (value) {
|
||||
if (value === undefined)
|
||||
return 'undefined';
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
var parse = function (serialized) {
|
||||
if (serialized === undefined || serialized === 'undefined')
|
||||
return undefined;
|
||||
return JSON.parse(serialized);
|
||||
};
|
||||
|
||||
set: function (key, value) {
|
||||
var self = this;
|
||||
Session = _.extend({}, {
|
||||
keys: {}, // key -> value
|
||||
keyDeps: {}, // key -> _ContextSet
|
||||
keyValueDeps: {}, // key -> value -> _ContextSet
|
||||
|
||||
if (typeof value !== 'string' &&
|
||||
typeof value !== 'number' &&
|
||||
typeof value !== 'boolean' &&
|
||||
value !== null && value !== undefined)
|
||||
throw new Error("Session.set: value can't be an object");
|
||||
set: function (key, value) {
|
||||
var self = this;
|
||||
|
||||
var oldValue = self.keys[key];
|
||||
if (value === oldValue)
|
||||
return;
|
||||
self.keys[key] = value;
|
||||
value = stringify(value);
|
||||
|
||||
var invalidateAll = function (cset) {
|
||||
cset && cset.invalidateAll();
|
||||
};
|
||||
var oldSerializedValue = 'undefined';
|
||||
if (_.has(self.keys, key)) oldSerializedValue = self.keys[key];
|
||||
if (value === oldSerializedValue)
|
||||
return;
|
||||
self.keys[key] = value;
|
||||
|
||||
invalidateAll(self.keyDeps[key]);
|
||||
if (self.keyValueDeps[key]) {
|
||||
invalidateAll(self.keyValueDeps[key][oldValue]);
|
||||
invalidateAll(self.keyValueDeps[key][value]);
|
||||
}
|
||||
},
|
||||
var invalidateAll = function (cset) {
|
||||
cset && cset.invalidateAll();
|
||||
};
|
||||
|
||||
get: function (key) {
|
||||
var self = this;
|
||||
self._ensureKey(key);
|
||||
self.keyDeps[key].addCurrentContext();
|
||||
return self.keys[key];
|
||||
},
|
||||
invalidateAll(self.keyDeps[key]);
|
||||
if (self.keyValueDeps[key]) {
|
||||
invalidateAll(self.keyValueDeps[key][oldSerializedValue]);
|
||||
invalidateAll(self.keyValueDeps[key][value]);
|
||||
}
|
||||
},
|
||||
|
||||
equals: function (key, value) {
|
||||
var self = this;
|
||||
var context = Meteor.deps.Context.current;
|
||||
|
||||
if (typeof value !== 'string' &&
|
||||
typeof value !== 'number' &&
|
||||
typeof value !== 'boolean' &&
|
||||
typeof value !== 'undefined' &&
|
||||
value !== null)
|
||||
throw new Error("Session.equals: value can't be an object");
|
||||
|
||||
if (context) {
|
||||
get: function (key) {
|
||||
var self = this;
|
||||
self._ensureKey(key);
|
||||
self.keyDeps[key].addCurrentContext();
|
||||
return parse(self.keys[key]);
|
||||
},
|
||||
|
||||
if (!(value in self.keyValueDeps[key]))
|
||||
self.keyValueDeps[key][value] = new Meteor.deps._ContextSet;
|
||||
equals: function (key, value) {
|
||||
var self = this;
|
||||
var context = Meteor.deps.Context.current;
|
||||
|
||||
var isNew = self.keyValueDeps[key][value].add(context);
|
||||
if (isNew) {
|
||||
context.onInvalidate(function () {
|
||||
// clean up [key][value] if it's now empty, so we don't use
|
||||
// O(n) memory for n = values seen ever
|
||||
if (self.keyValueDeps[key][value].isEmpty())
|
||||
delete self.keyValueDeps[key][value];
|
||||
});
|
||||
// We don't allow objects (or arrays that might include objects) for
|
||||
// .equals, because JSON.stringify doesn't canonicalize object key
|
||||
// order. (We can make equals have the right return value by parsing the
|
||||
// current value and using _.isEqual, but we won't have a canonical
|
||||
// element of keyValueDeps[key] to store the context.) You can still use
|
||||
// "_.isEqual(Session.get(key), value)".
|
||||
//
|
||||
// XXX we could allow arrays as long as we recursively check that there
|
||||
// are no objects
|
||||
if (typeof value !== 'string' &&
|
||||
typeof value !== 'number' &&
|
||||
typeof value !== 'boolean' &&
|
||||
typeof value !== 'undefined' &&
|
||||
value !== null)
|
||||
throw new Error("Session.equals: value must be scalar");
|
||||
var serializedValue = stringify(value);
|
||||
|
||||
if (context) {
|
||||
self._ensureKey(key);
|
||||
|
||||
if (! _.has(self.keyValueDeps[key], serializedValue))
|
||||
self.keyValueDeps[key][serializedValue] = new Meteor.deps._ContextSet;
|
||||
|
||||
var isNew = self.keyValueDeps[key][serializedValue].add(context);
|
||||
if (isNew) {
|
||||
context.onInvalidate(function () {
|
||||
// clean up [key][serializedValue] if it's now empty, so we don't
|
||||
// use O(n) memory for n = values seen ever
|
||||
if (self.keyValueDeps[key][serializedValue].isEmpty())
|
||||
delete self.keyValueDeps[key][serializedValue];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var oldValue = undefined;
|
||||
if (_.has(self.keys, key)) oldValue = parse(self.keys[key]);
|
||||
return oldValue === value;
|
||||
},
|
||||
|
||||
_ensureKey: function (key) {
|
||||
var self = this;
|
||||
if (!(key in self.keyDeps)) {
|
||||
self.keyDeps[key] = new Meteor.deps._ContextSet;
|
||||
self.keyValueDeps[key] = {};
|
||||
}
|
||||
}
|
||||
|
||||
return self.keys[key] === value;
|
||||
},
|
||||
|
||||
_ensureKey: function (key) {
|
||||
var self = this;
|
||||
if (!(key in self.keyDeps)) {
|
||||
self.keyDeps[key] = new Meteor.deps._ContextSet;
|
||||
self.keyValueDeps[key] = {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (Meteor._reload) {
|
||||
Meteor._reload.onMigrate('session', function () {
|
||||
// XXX sanitize and make sure it's JSONible?
|
||||
return [true, {keys: Session.keys}];
|
||||
});
|
||||
|
||||
(function () {
|
||||
var migrationData = Meteor._reload.migrationData('session');
|
||||
if (migrationData && migrationData.keys) {
|
||||
Session.keys = migrationData.keys;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
if (Meteor._reload) {
|
||||
Meteor._reload.onMigrate('session', function () {
|
||||
// XXX sanitize and make sure it's JSONible?
|
||||
return [true, {keys: Session.keys}];
|
||||
});
|
||||
|
||||
(function () {
|
||||
var migrationData = Meteor._reload.migrationData('session');
|
||||
if (migrationData && migrationData.keys) {
|
||||
Session.keys = migrationData.keys;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
}());
|
||||
|
||||
158
packages/session/session_tests.js
Normal file
158
packages/session/session_tests.js
Normal file
@@ -0,0 +1,158 @@
|
||||
(function () {
|
||||
|
||||
Tinytest.add('session - get/set/equals types', function (test) {
|
||||
test.equal(Session.get('u'), undefined);
|
||||
test.isTrue(Session.equals('u', undefined));
|
||||
test.isFalse(Session.equals('u', null));
|
||||
test.isFalse(Session.equals('u', 0));
|
||||
test.isFalse(Session.equals('u', ''));
|
||||
|
||||
Session.set('u', undefined);
|
||||
test.equal(Session.get('u'), undefined);
|
||||
test.isTrue(Session.equals('u', undefined));
|
||||
test.isFalse(Session.equals('u', null));
|
||||
test.isFalse(Session.equals('u', 0));
|
||||
test.isFalse(Session.equals('u', ''));
|
||||
test.isFalse(Session.equals('u', 'undefined'));
|
||||
test.isFalse(Session.equals('u', 'null'));
|
||||
|
||||
Session.set('n', null);
|
||||
test.equal(Session.get('n'), null);
|
||||
test.isFalse(Session.equals('n', undefined));
|
||||
test.isTrue(Session.equals('n', null));
|
||||
test.isFalse(Session.equals('n', 0));
|
||||
test.isFalse(Session.equals('n', ''));
|
||||
test.isFalse(Session.equals('n', 'undefined'));
|
||||
test.isFalse(Session.equals('n', 'null'));
|
||||
|
||||
Session.set('t', true);
|
||||
test.equal(Session.get('t'), true);
|
||||
test.isTrue(Session.equals('t', true));
|
||||
test.isFalse(Session.equals('t', false));
|
||||
test.isFalse(Session.equals('t', 1));
|
||||
test.isFalse(Session.equals('t', 'true'));
|
||||
|
||||
Session.set('f', false);
|
||||
test.equal(Session.get('f'), false);
|
||||
test.isFalse(Session.equals('f', true));
|
||||
test.isTrue(Session.equals('f', false));
|
||||
test.isFalse(Session.equals('f', 1));
|
||||
test.isFalse(Session.equals('f', 'false'));
|
||||
|
||||
Session.set('num', 0);
|
||||
test.equal(Session.get('num'), 0);
|
||||
test.isTrue(Session.equals('num', 0));
|
||||
test.isFalse(Session.equals('num', false));
|
||||
test.isFalse(Session.equals('num', '0'));
|
||||
test.isFalse(Session.equals('num', 1));
|
||||
|
||||
Session.set('str', 'true');
|
||||
test.equal(Session.get('str'), 'true');
|
||||
test.isTrue(Session.equals('str', 'true'));
|
||||
test.isFalse(Session.equals('str', true));
|
||||
|
||||
Session.set('arr', [1, 2, {a: 1, b: [5, 6]}]);
|
||||
test.equal(Session.get('arr'), [1, 2, {b: [5, 6], a: 1}]);
|
||||
test.isFalse(Session.equals('arr', 1));
|
||||
test.isFalse(Session.equals('arr', '[1,2,{"a":1,"b":[5,6]}]'));
|
||||
test.throws(function () {
|
||||
Session.equals('arr', [1, 2, {a: 1, b: [5, 6]}]);
|
||||
});
|
||||
|
||||
Session.set('obj', {a: 1, b: [5, 6]});
|
||||
test.equal(Session.get('obj'), {b: [5, 6], a: 1});
|
||||
test.isFalse(Session.equals('obj', 1));
|
||||
test.isFalse(Session.equals('obj', '{"a":1,"b":[5,6]}'));
|
||||
test.throws(function() { Session.equals('obj', {a: 1, b: [5, 6]}); });
|
||||
});
|
||||
|
||||
Tinytest.add('session - objects are cloned', function (test) {
|
||||
Session.set('frozen-array', [1, 2, 3]);
|
||||
Session.get('frozen-array')[1] = 42;
|
||||
test.equal(Session.get('frozen-array'), [1, 2, 3]);
|
||||
|
||||
Session.set('frozen-object', {a: 1, b: 2});
|
||||
Session.get('frozen-object').a = 43;
|
||||
test.equal(Session.get('frozen-object'), {a: 1, b: 2});
|
||||
});
|
||||
|
||||
Tinytest.add('session - context invalidation for get', function (test) {
|
||||
var xGetExecutions = 0;
|
||||
Meteor._autorun(function () {
|
||||
++xGetExecutions;
|
||||
Session.get('x');
|
||||
});
|
||||
test.equal(xGetExecutions, 1);
|
||||
Session.set('x', 1);
|
||||
// Invalidation shouldn't happen until flush time.
|
||||
test.equal(xGetExecutions, 1);
|
||||
Meteor.flush();
|
||||
test.equal(xGetExecutions, 2);
|
||||
// Setting to the same value doesn't re-run.
|
||||
Session.set('x', 1);
|
||||
Meteor.flush();
|
||||
test.equal(xGetExecutions, 2);
|
||||
Session.set('x', '1');
|
||||
Meteor.flush();
|
||||
test.equal(xGetExecutions, 3);
|
||||
});
|
||||
|
||||
Tinytest.add('session - context invalidation for equals', function (test) {
|
||||
var xEqualsExecutions = 0;
|
||||
Meteor._autorun(function () {
|
||||
++xEqualsExecutions;
|
||||
Session.equals('x', 5);
|
||||
});
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Session.set('x', 1);
|
||||
Meteor.flush();
|
||||
// Changing undefined -> 1 shouldn't affect equals(5).
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Session.set('x', 5);
|
||||
// Invalidation shouldn't happen until flush time.
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Meteor.flush();
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Session.set('x', 5);
|
||||
Meteor.flush();
|
||||
// Setting to the same value doesn't re-run.
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Session.set('x', '5');
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Meteor.flush();
|
||||
test.equal(xEqualsExecutions, 3);
|
||||
Session.set('x', 5);
|
||||
test.equal(xEqualsExecutions, 3);
|
||||
Meteor.flush();
|
||||
test.equal(xEqualsExecutions, 4);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
'session - context invalidation for equals with undefined',
|
||||
function (test) {
|
||||
// Make sure the special casing for equals undefined works.
|
||||
var yEqualsExecutions = 0;
|
||||
Meteor._autorun(function () {
|
||||
++yEqualsExecutions;
|
||||
Session.equals('y', undefined);
|
||||
});
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set('y', undefined);
|
||||
Meteor.flush();
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set('y', 5);
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Meteor.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', 3);
|
||||
Meteor.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', 'undefined');
|
||||
Meteor.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', undefined);
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Meteor.flush();
|
||||
test.equal(yEqualsExecutions, 3);
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user