mirror of https://github.com/zulip/zulip.git
js: Extract FoldDict class.
We have ~5 years of proof that we'll probably never extend Dict with more options. Breaking the classes into makes both a little faster (no options to check), and we remove some options in FoldDict that are never used (from/from_array). A possible next step is to fine-tune the Dict to use Map internally. Note that the TypeScript types for FoldDict are now more specific (requiring string keys). Of course, this isn't really enforced until we convert other modules to TS.
This commit is contained in:
parent
9cd075ffb1
commit
ee3e488e02
|
@ -13,10 +13,12 @@ run_test('basic', () => {
|
|||
|
||||
d.set('foo', 'baz');
|
||||
assert.equal(d.get('foo'), 'baz');
|
||||
assert.equal(d.num_items(), 1);
|
||||
|
||||
d.set('bar', 'qux');
|
||||
assert.equal(d.get('foo'), 'baz');
|
||||
assert.equal(d.get('bar'), 'qux');
|
||||
assert.equal(d.num_items(), 2);
|
||||
|
||||
assert.equal(d.has('bar'), true);
|
||||
assert.equal(d.has('baz'), false);
|
||||
|
@ -52,41 +54,14 @@ run_test('filter_values', () => {
|
|||
assert.deepEqual(d.filter_values(pred).sort(), ['fay', 'foo', 'fred']);
|
||||
});
|
||||
|
||||
run_test('fold_case', () => {
|
||||
const d = new Dict({fold_case: true});
|
||||
|
||||
assert.deepEqual(d.keys(), []);
|
||||
|
||||
assert(!d.has('foo'));
|
||||
d.set('fOO', 'Hello World');
|
||||
assert.equal(d.get('foo'), 'Hello World');
|
||||
assert(d.has('foo'));
|
||||
assert(d.has('FOO'));
|
||||
assert(!d.has('not_a_key'));
|
||||
|
||||
assert.deepEqual(d.keys(), ['fOO']);
|
||||
|
||||
d.del('Foo');
|
||||
assert.equal(d.has('foo'), false);
|
||||
|
||||
assert.deepEqual(d.keys(), []);
|
||||
});
|
||||
|
||||
run_test('undefined_keys', () => {
|
||||
blueslip.set_test_data('error', 'Tried to call a Dict method with an undefined key.');
|
||||
|
||||
let d = new Dict();
|
||||
const d = new Dict();
|
||||
|
||||
assert.equal(d.has(undefined), false);
|
||||
assert.strictEqual(d.get(undefined), undefined);
|
||||
|
||||
d = new Dict({fold_case: true});
|
||||
|
||||
assert.equal(d.has(undefined), false);
|
||||
assert.strictEqual(d.get(undefined), undefined);
|
||||
assert.equal(blueslip.get_test_logs('error').length, 4);
|
||||
|
||||
blueslip.clear_test_data();
|
||||
assert.equal(blueslip.get_test_logs('error').length, 2);
|
||||
});
|
||||
|
||||
run_test('restricted_keys', () => {
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
const FoldDict = zrequire('fold_dict').FoldDict;
|
||||
set_global('blueslip', global.make_zblueslip());
|
||||
|
||||
run_test('basic', () => {
|
||||
const d = new FoldDict();
|
||||
|
||||
assert(d.is_empty());
|
||||
|
||||
assert.deepEqual(d.keys(), []);
|
||||
|
||||
d.set('foo', 'bar');
|
||||
assert.equal(d.get('foo'), 'bar');
|
||||
assert(!d.is_empty());
|
||||
|
||||
d.set('foo', 'baz');
|
||||
assert.equal(d.get('foo'), 'baz');
|
||||
assert.equal(d.num_items(), 1);
|
||||
|
||||
d.set('bar', 'qux');
|
||||
assert.equal(d.get('foo'), 'baz');
|
||||
assert.equal(d.get('bar'), 'qux');
|
||||
assert.equal(d.num_items(), 2);
|
||||
|
||||
assert.equal(d.has('bar'), true);
|
||||
assert.equal(d.has('baz'), false);
|
||||
|
||||
assert.deepEqual(d.keys(), ['foo', 'bar']);
|
||||
assert.deepEqual(d.values(), ['baz', 'qux']);
|
||||
assert.deepEqual(d.items(), [['foo', 'baz'], ['bar', 'qux']]);
|
||||
|
||||
d.del('bar');
|
||||
assert.equal(d.has('bar'), false);
|
||||
assert.strictEqual(d.get('bar'), undefined);
|
||||
|
||||
assert.deepEqual(d.keys(), ['foo']);
|
||||
|
||||
const val = ['foo'];
|
||||
const res = d.set('abc', val);
|
||||
assert.equal(val, res);
|
||||
});
|
||||
|
||||
run_test('case insensitivity', () => {
|
||||
const d = new FoldDict();
|
||||
|
||||
assert.deepEqual(d.keys(), []);
|
||||
|
||||
assert(!d.has('foo'));
|
||||
d.set('fOO', 'Hello World');
|
||||
assert.equal(d.get('foo'), 'Hello World');
|
||||
assert(d.has('foo'));
|
||||
assert(d.has('FOO'));
|
||||
assert(!d.has('not_a_key'));
|
||||
|
||||
assert.deepEqual(d.keys(), ['fOO']);
|
||||
|
||||
d.del('Foo');
|
||||
assert.equal(d.has('foo'), false);
|
||||
|
||||
assert.deepEqual(d.keys(), []);
|
||||
});
|
||||
|
||||
run_test('clear', () => {
|
||||
const d = new FoldDict();
|
||||
|
||||
function populate() {
|
||||
d.set('fOO', 1);
|
||||
assert.equal(d.get('foo'), 1);
|
||||
d.set('bAR', 2);
|
||||
assert.equal(d.get('bar'), 2);
|
||||
}
|
||||
|
||||
populate();
|
||||
assert.equal(d.num_items(), 2);
|
||||
assert(!d.is_empty());
|
||||
|
||||
d.clear();
|
||||
assert.equal(d.get('fOO'), undefined);
|
||||
assert.equal(d.get('bAR'), undefined);
|
||||
assert.equal(d.num_items(), 0);
|
||||
assert(d.is_empty());
|
||||
|
||||
// make sure it still works after clearing
|
||||
populate();
|
||||
assert.equal(d.num_items(), 2);
|
||||
});
|
||||
|
||||
run_test('undefined_keys', () => {
|
||||
blueslip.set_test_data('error', 'Tried to call a FoldDict method with an undefined key.');
|
||||
|
||||
const d = new FoldDict();
|
||||
|
||||
assert.equal(d.has(undefined), false);
|
||||
assert.strictEqual(d.get(undefined), undefined);
|
||||
assert.equal(blueslip.get_test_logs('error').length, 2);
|
||||
|
||||
blueslip.clear_test_data();
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
set_global('document', 'document-stub');
|
||||
set_global('$', global.make_zjquery());
|
||||
|
||||
const FoldDict = zrequire('fold_dict').FoldDict;
|
||||
zrequire('unread_ui');
|
||||
zrequire('Filter', 'js/filter');
|
||||
zrequire('util');
|
||||
|
@ -670,7 +671,7 @@ run_test('update_count_in_dom', () => {
|
|||
};
|
||||
};
|
||||
|
||||
const topic_count = new Dict({fold_case: true});
|
||||
const topic_count = new FoldDict();
|
||||
topic_count.set('lunch', '555');
|
||||
counts.topic_count.set(stream_id, topic_count);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ zrequire('stream_data');
|
|||
zrequire('util');
|
||||
zrequire('unread');
|
||||
zrequire('settings_notifications');
|
||||
const FoldDict = zrequire('fold_dict').FoldDict;
|
||||
|
||||
set_global('page_params', {});
|
||||
set_global('blueslip', {});
|
||||
|
@ -285,7 +286,7 @@ run_test('num_unread_for_topic', () => {
|
|||
msg_ids = unread.get_msg_ids_for_stream(stream_id);
|
||||
assert.deepEqual(msg_ids, _.range(1, 501));
|
||||
|
||||
const topic_dict = new Dict({fold_case: true});
|
||||
const topic_dict = new FoldDict();
|
||||
|
||||
let missing_topics = unread.get_missing_topics({
|
||||
stream_id: stream_id,
|
||||
|
|
|
@ -33,6 +33,7 @@ import "../lightbox_canvas.js";
|
|||
import "../rtl.js";
|
||||
import "../lazy_set.js";
|
||||
import "../dict.ts";
|
||||
import "../fold_dict.ts";
|
||||
import "../scroll_util.js";
|
||||
import "../components.js";
|
||||
import "../feedback_widget.js";
|
||||
|
|
|
@ -1,44 +1,23 @@
|
|||
import * as _ from 'underscore';
|
||||
|
||||
/**
|
||||
* Implementation detail of the Dict class. `key` is `k` converted to a string,
|
||||
* in lowercase if the `fold_case` option is enabled.
|
||||
*/
|
||||
type KeyValue<K, V> = { k: K; v: V };
|
||||
type Items<K, V> = {
|
||||
[key: string]: KeyValue<K, V>;
|
||||
};
|
||||
|
||||
/**
|
||||
* This class primarily exists to support the fold_case option, because so many
|
||||
* string keys in Zulip are case-insensitive (emails, stream names, topics,
|
||||
* etc.). Dict also accepts any key that can be converted to a string.
|
||||
*/
|
||||
export class Dict<K, V> {
|
||||
private _items: Items<K, V> = {};
|
||||
private _fold_case: boolean;
|
||||
|
||||
/**
|
||||
* @param opts - setting `fold_case` to true will make `has()` and `get()`
|
||||
* case-insensitive. `keys()` and other methods that
|
||||
* implicitly return keys return the original casing/type
|
||||
* of the key passed into `set()`.
|
||||
*/
|
||||
constructor(opts?: {fold_case: boolean}) {
|
||||
this._fold_case = opts ? opts.fold_case : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Dict object from an existing object's keys and values.
|
||||
* @param obj - A javascript object
|
||||
* @param opts - Options to be passed to the Dict constructor
|
||||
*/
|
||||
static from<V>(obj: { [key: string]: V }, opts?: {fold_case: boolean}): Dict<string, V> {
|
||||
static from<V>(obj: { [key: string]: V }): Dict<string, V> {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
throw new TypeError("Cannot convert argument to Dict");
|
||||
}
|
||||
|
||||
const dict = new Dict<string, V>(opts);
|
||||
const dict = new Dict<string, V>();
|
||||
_.each(obj, function (val: V, key: string) {
|
||||
dict.set(key, val);
|
||||
});
|
||||
|
@ -50,14 +29,13 @@ export class Dict<K, V> {
|
|||
* Construct a Dict object from an array with each element set to `true`.
|
||||
* Intended for use as a set data structure.
|
||||
* @param arr - An array of keys
|
||||
* @param opts - Options to be passed to the Dict constructor
|
||||
*/
|
||||
static from_array<K, V>(arr: K[], opts?: {fold_case: boolean}): Dict<K, V | true> {
|
||||
static from_array<K, V>(arr: K[]): Dict<K, V | true> {
|
||||
if (!(arr instanceof Array)) {
|
||||
throw new TypeError("Argument is not an array");
|
||||
}
|
||||
|
||||
const dict = new Dict<K, V | true>(opts);
|
||||
const dict = new Dict<K, V | true>();
|
||||
for (const key of arr) {
|
||||
dict.set(key, true);
|
||||
}
|
||||
|
@ -65,7 +43,7 @@ export class Dict<K, V> {
|
|||
}
|
||||
|
||||
clone(): Dict<K, V> {
|
||||
const dict = new Dict<K, V>({fold_case: this._fold_case});
|
||||
const dict = new Dict<K, V>();
|
||||
dict._items = { ...this._items };
|
||||
return dict;
|
||||
}
|
||||
|
@ -144,13 +122,13 @@ export class Dict<K, V> {
|
|||
this._items = {};
|
||||
}
|
||||
|
||||
// Handle case-folding of keys and the empty string.
|
||||
// Convert keys to strings and handle undefined.
|
||||
private _munge(key: K): string | undefined {
|
||||
if (key === undefined) {
|
||||
blueslip.error("Tried to call a Dict method with an undefined key.");
|
||||
return undefined;
|
||||
}
|
||||
const str_key = ':' + key.toString();
|
||||
return this._fold_case ? str_key.toLowerCase() : str_key;
|
||||
return str_key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import * as _ from 'underscore';
|
||||
|
||||
/*
|
||||
Use this class to manage keys where you don't care
|
||||
about case (i.e. case-insensitive).
|
||||
|
||||
Keys for FoldDict should be strings. We "fold" all
|
||||
casings of "alice" (e.g. "ALICE", "Alice", "ALIce", etc.)
|
||||
to "alice" as the key.
|
||||
|
||||
Examples of case-insensitive data in Zulip are:
|
||||
- emails
|
||||
- stream names
|
||||
- topics
|
||||
- etc.
|
||||
*/
|
||||
type KeyValue<V> = { k: string; v: V };
|
||||
type Items<V> = {
|
||||
[key: string]: KeyValue<V>;
|
||||
};
|
||||
|
||||
export class FoldDict<V> {
|
||||
private _items: Items<V> = {};
|
||||
|
||||
get(key: string): V | undefined {
|
||||
const mapping = this._items[this._munge(key)];
|
||||
if (mapping === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return mapping.v;
|
||||
}
|
||||
|
||||
set(key: string, value: V): V {
|
||||
this._items[this._munge(key)] = {k: key, v: value};
|
||||
return value;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return _.has(this._items, this._munge(key));
|
||||
}
|
||||
|
||||
del(key: string): void {
|
||||
delete this._items[this._munge(key)];
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
return _.pluck(_.values(this._items), 'k');
|
||||
}
|
||||
|
||||
values(): V[] {
|
||||
return _.pluck(_.values(this._items), 'v');
|
||||
}
|
||||
|
||||
items(): [string, V][] {
|
||||
return _.map(_.values(this._items),
|
||||
(mapping: KeyValue<V>): [string, V] => [mapping.k, mapping.v]);
|
||||
}
|
||||
|
||||
num_items(): number {
|
||||
return _.keys(this._items).length;
|
||||
}
|
||||
|
||||
is_empty(): boolean {
|
||||
return _.isEmpty(this._items);
|
||||
}
|
||||
|
||||
each(f: (v: V, k?: string) => void): void {
|
||||
_.each(this._items, (mapping: KeyValue<V>) => f(mapping.v, mapping.k));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._items = {};
|
||||
}
|
||||
|
||||
// Handle case-folding of keys and the empty string.
|
||||
private _munge(key: string): string | undefined {
|
||||
if (key === undefined) {
|
||||
blueslip.error("Tried to call a FoldDict method with an undefined key.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const str_key = ':' + key.toString().toLowerCase();
|
||||
return str_key;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
let muted_topics = new Dict();
|
||||
|
||||
exports.add_muted_topic = function (stream_id, topic) {
|
||||
let sub_dict = muted_topics.get(stream_id);
|
||||
if (!sub_dict) {
|
||||
sub_dict = new Dict({fold_case: true});
|
||||
sub_dict = new FoldDict();
|
||||
muted_topics.set(stream_id, sub_dict);
|
||||
}
|
||||
sub_dict.set(topic, true);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require("unorm"); // String.prototype.normalize polyfill for IE11
|
||||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
let people_dict;
|
||||
let people_by_name_dict;
|
||||
|
@ -17,8 +18,8 @@ exports.init = function () {
|
|||
// (all people we've seen), but people_dict can have duplicate
|
||||
// keys related to email changes. We want to deprecate
|
||||
// people_dict over time and always do lookups by user_id.
|
||||
people_dict = new Dict({fold_case: true});
|
||||
people_by_name_dict = new Dict({fold_case: true});
|
||||
people_dict = new FoldDict();
|
||||
people_by_name_dict = new FoldDict();
|
||||
people_by_user_id_dict = new Dict();
|
||||
|
||||
// The next dictionary includes all active users (human/user)
|
||||
|
@ -29,7 +30,7 @@ exports.init = function () {
|
|||
pm_recipient_count_dict = new Dict();
|
||||
|
||||
// The next Dict maintains a set of ids of people with same full names.
|
||||
duplicate_full_name_data = new Dict({fold_case: true});
|
||||
duplicate_full_name_data = new FoldDict();
|
||||
};
|
||||
|
||||
// WE INITIALIZE DATA STRUCTURES HERE!
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
const partners = new Dict();
|
||||
|
||||
|
@ -15,7 +16,7 @@ exports.recent = (function () {
|
|||
// recent conversations with, sorted by time (implemented via
|
||||
// `message_id` sorting, since that's how we time-sort messages).
|
||||
const self = {};
|
||||
const recent_message_ids = new Dict({fold_case: true}); // key is user_ids_string
|
||||
const recent_message_ids = new FoldDict(); // key is user_ids_string
|
||||
const recent_private_messages = [];
|
||||
|
||||
self.insert = function (user_ids, message_id) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
const topic_senders = new Dict(); // key is stream-id, value is Dict
|
||||
const stream_senders = new Dict(); // key is stream-id, value is Dict
|
||||
|
@ -8,7 +9,7 @@ exports.process_message_for_senders = function (message) {
|
|||
const topic = util.get_message_topic(message);
|
||||
|
||||
// Process most recent sender to topic
|
||||
const topic_dict = topic_senders.get(stream_id) || new Dict({fold_case: true});
|
||||
const topic_dict = topic_senders.get(stream_id) || new FoldDict();
|
||||
let sender_message_ids = topic_dict.get(topic) || new Dict();
|
||||
let old_message_id = sender_message_ids.get(message.sender_id);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
const LazySet = require('./lazy_set').LazySet;
|
||||
|
||||
const BinaryDict = function (pred) {
|
||||
|
@ -18,8 +19,8 @@ const BinaryDict = function (pred) {
|
|||
*/
|
||||
|
||||
const self = {};
|
||||
self.trues = new Dict({fold_case: true});
|
||||
self.falses = new Dict({fold_case: true});
|
||||
self.trues = new FoldDict();
|
||||
self.falses = new FoldDict();
|
||||
|
||||
self.true_values = function () {
|
||||
return self.trues.values();
|
||||
|
@ -83,13 +84,12 @@ let stream_info;
|
|||
let subs_by_stream_id;
|
||||
let filter_out_inactives = false;
|
||||
|
||||
const stream_ids_by_name = new Dict({fold_case: true});
|
||||
const stream_ids_by_name = new FoldDict();
|
||||
|
||||
exports.clear_subscriptions = function () {
|
||||
stream_info = new BinaryDict(function (sub) {
|
||||
return sub.subscribed;
|
||||
});
|
||||
|
||||
subs_by_stream_id = new Dict();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
let stream_dict = new Dict(); // stream_id -> array of objects
|
||||
|
||||
|
@ -13,7 +14,7 @@ exports.stream_has_topics = function (stream_id) {
|
|||
};
|
||||
|
||||
exports.topic_history = function (stream_id) {
|
||||
const topics = new Dict({fold_case: true});
|
||||
const topics = new FoldDict();
|
||||
|
||||
const self = {};
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const render_more_topics = require('../templates/more_topics.hbs');
|
||||
const render_topic_list_item = require('../templates/topic_list_item.hbs');
|
||||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
/*
|
||||
Track all active widgets with a Dict.
|
||||
|
@ -84,7 +85,7 @@ exports.widget = function (parent_elem, my_stream_id) {
|
|||
const self = {};
|
||||
|
||||
self.build_list = function () {
|
||||
self.topic_items = new Dict({fold_case: true});
|
||||
self.topic_items = new FoldDict();
|
||||
let topics_selected = 0;
|
||||
let more_topics_unreads = 0;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
// See https://zulip.readthedocs.io/en/latest/subsystems/pointer.html for notes on
|
||||
// how this system is designed.
|
||||
|
@ -65,8 +66,7 @@ const unread_messages = make_id_set();
|
|||
|
||||
function make_bucketer(options) {
|
||||
const self = {};
|
||||
|
||||
const key_to_bucket = new Dict({fold_case: options.fold_case});
|
||||
const key_to_bucket = options.fold_case ? new FoldDict() : new Dict();
|
||||
const reverse_lookup = new Dict();
|
||||
|
||||
self.clear = function () {
|
||||
|
@ -273,7 +273,7 @@ exports.unread_topic_counter = (function () {
|
|||
|
||||
function str_dict() {
|
||||
// Use this when keys are topics
|
||||
return new Dict({fold_case: true});
|
||||
return new FoldDict();
|
||||
}
|
||||
|
||||
function num_dict() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const Dict = require('./dict').Dict;
|
||||
const FoldDict = require('./fold_dict').FoldDict;
|
||||
|
||||
let user_group_name_dict;
|
||||
let user_group_by_id_dict;
|
||||
|
@ -6,7 +7,7 @@ let user_group_by_id_dict;
|
|||
// We have an init() function so that our automated tests
|
||||
// can easily clear data.
|
||||
exports.init = function () {
|
||||
user_group_name_dict = new Dict({fold_case: true});
|
||||
user_group_name_dict = new FoldDict();
|
||||
user_group_by_id_dict = new Dict();
|
||||
};
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ enforce_fully_covered = {
|
|||
'static/js/fenced_code.js',
|
||||
'static/js/fetch_status.js',
|
||||
'static/js/filter.js',
|
||||
'static/js/fold_dict.ts',
|
||||
'static/js/hash_util.js',
|
||||
'static/js/keydown_util.js',
|
||||
'static/js/input_pill.js',
|
||||
|
|
Loading…
Reference in New Issue