module.exports.ResponseCache = ks_utils.Class({ default_options: { max_age: 600 }, methods: [ 'GET', 'HEAD' ], ERR_NOT_MODIFIED: 'NOT_MODIFIED', ERR_STALE: 'STALE', ERR_MISS: 'MISS', initialize: function (options) { // Create a memcache instance, if necessary if (this.options.memcache) { var mo = this.options.memcache; this.memcached = new Memcached(mo.server, mo.options || {}); } else { // If the configuration is missing, use the fake stub cache this.memcached = new ks_utils.FakeMemcached(); } }, // ### cacheResponse // // Wrapper for a response handler. Will cache fresh responses from the // handler, and skip calling the handler altogether if the cache is valid. // // Honors HTTP cache control and conditional GET semantics. // // This kind of wants to be connect/express middleware, but it doesn't // *quite* work that way. // cacheResponse: function (req, res, options, response_cb) { var $this = this; // Shortcircuit on unsupported request methods var method = req.method.toUpperCase(), supported_methods = $this.methods; if (supported_methods.indexOf(method) === -1) { return $this.revalidate(null, {}, req, res, null, response_cb); } // Start building a cache key with the URL path. var cache_key_parts = [ url.parse(req.url).pathname ]; // Include the values of request headers specified in the Vary: // response header. var vary_names = (''+res.header('vary')).split(',') .map(function (name) { return name.trim().toLowerCase(); }); vary_names.sort(); vary_names.forEach(function (name) { cache_key_parts.push(name + ': ' + req.header(name)); }); // Build a cache key based on all the parts. var cache_key = 'response-cache:' + crypto.createHash('sha1') .update(cache_key_parts.join('|')).digest('hex'); // Handy for diagnostics, maybe not needed res.header('X-Cache-Key', cache_key); var ua_cc = parseCacheControl(req.headers['cache-control'] || ''); var ims = req.header('if-modified-since'); var opts = { if_none_match: req.header('if-none-match'), if_modified_since: ims ? (new Date(ims)).getTime() : null, no_cache: !_.isUndefined(ua_cc['no-cache']), max_age: _.isUndefined(ua_cc['max-age']) ? $this.options.max_age : ua_cc['max-age'] }; $this.get(cache_key, opts, function (err, headers, body, meta) { if (err == $this.ERR_MISS || err == $this.ERR_STALE) { return $this.revalidate(cache_key, opts, req, res, err, response_cb); } else if (err == $this.ERR_NOT_MODIFIED) { return $this.sendNotModified(req, res, meta); } else { return $this.sendCacheEntry(cache_key, req, res, headers, body, meta); } }); }, // ### revalidate // // Runs the real response handler, caches the result if necessary. // // Lots of monkeypatching here to intercept the response events and build // the cache entry for storage. // revalidate: function (cache_key, opts, req, res, err, response_cb) { var $this = this, cached_headers = [], cached_meta = { }, cached_body_chunks = [], status_code = 999; // If there's no cache_key, skip all the caching monkeybusiness, if (!cache_key) { return response_cb(req, res); } // Monkeypatch to capture response headers var orig_setHeader = res.setHeader; res.setHeader = function (name, value) { orig_setHeader.apply(this, arguments); cached_headers.push([name, value]); name_lc = name.toLowerCase(); if ('etag' == name_lc) { cached_meta.etag = value; } if ('last-modified' == name_lc) { cached_meta.last_modified = (new Date(value)).getTime(); } }; // Monkeypatch to set a Last-Modified header, if none in response var orig_writeHead = res.writeHead; res.writeHead = function (status, headers) { status_code = status; if (200 == status && _.isUndefined(cached_meta.last_modified)) { res.setHeader('Last-Modified', (new Date()).toUTCString()); } orig_writeHead.apply(this, arguments); }; // Cache body chunks as they come in. var cache_chunk = function (chunk, encoding) { // TODO: This *could* be trouble, if the response encoding isn't // UTF-8. The cache will have it encoded as UTF-8, but headers will // say otherwise. Probably good for now, though, since we're using // UTF-8 end-to-end in kumascript. cached_body_chunks.push(new Buffer(chunk, encoding) .toString('utf-8')); }; // Monkeypatch to capture written body chunks var orig_write = res.write; res.write = function (chunk, encoding) { orig_write.apply(this, arguments); cache_chunk(chunk, encoding); }; // Monkeypatch to capture the final send and cache the response var orig_send = res.send; res.send = function (chunk, encoding) { orig_send.apply(this, arguments); if (200 == status_code) { // Catch the last chunk, if any. if (chunk) { cache_chunk(chunk, encoding); } var cached_body = cached_body_chunks.join(''); $this.set(cache_key, opts.max_age, cached_headers, cached_body, cached_meta, function (err, headers, body, meta) { /* No-op, fire and forget. */ } ); } }; // TODO: addTrailers? // Finally, signal to the origin handler that it's time to revalidate. return response_cb(req, res); }, // ### sendCacheEntry // // Send the content from the cached response entry // sendCacheEntry: function (cache_key, req, res, headers, body, meta) { var $this = this; $this.sendCommonHeaders(req, res, meta); res.header('X-Cache', 'HIT'); _.each(headers, function (header) { res.header(header[0], header[1]); }); var method = req.method.toUpperCase(); if ('HEAD' != method) { res.write(body, 'utf-8'); } res.end(); }, // ### sendNotModified // // When a conditional GET signals no need to send content, this handles // sending the 304 Not Modified. // sendNotModified: function (req, res, meta) { this.sendCommonHeaders(req, res, meta); return res.send(304); }, // ### sendCommonHeaders // // Send headers common to many responses. // sendCommonHeaders: function (req, res, meta) { var $this = this, now = (new Date()); res.header('Age', Math.floor((now - meta.last_modified) / 1000)); res.header('Date', new Date().toUTCString()); if (meta.last_modified) { res.header('Last-Modified', new Date(meta.last_modified).toUTCString()); } if (meta.etag) { res.header('ETag', meta.etag); } }, // ### set // // Store a cache entry // set: function (key, max_age, headers, body, meta, next) { var $this = this; meta = meta || {}; max_age = max_age || this.options.max_age; $this.memcached.set(key + ':headers', headers, max_age, function (e, r) { $this.memcached.set(key + ':body', body, max_age, function (e, r) { $this.memcached.set(key + ':meta', meta, max_age, function (e, r) { next(null, headers, body, meta); }); }); }); }, // ### get // // Get a cache entry // // Implements HTTP caching semantics and responds with content as well // as STALE, NOT_MODIFIED, MISS errors // get: function (key, options, next) { options = options || {}; var $this = this; if (0 === options.max_age) { // If I really wanted to, a $this._hasMeta(key) would let me report // ERR_STALE instead of ERR_MISS. But, I wanted to skip hitting the // cache backend altogether (eg. memcache or filesystem) return next($this.ERR_MISS); } // See above. if (options.no_cache) { return next($this.ERR_MISS); } $this.memcached.get(key + ':meta', function (err, meta) { // Punt, if $this key is not even in the cache if (!meta) { return next($this.ERR_MISS); } // Check if the cache is too old if (typeof(options.max_age) != 'undefined') { var now = (new Date()).getTime(), last_modified = meta.last_modified, age = (now - last_modified) / 1000; if (age > options.max_age) { return next($this.ERR_STALE, null, null, meta); } } // Check modified since, if necessary if (typeof(options.if_modified_since) != 'undefined' && (meta.last_modified <= options.if_modified_since)) { return next($this.ERR_NOT_MODIFIED, null, null, meta); } // Check the content etag, if necessary if (typeof(options.if_none_match) != 'undefined' && (options.if_none_match == meta.etag)) { return next($this.ERR_NOT_MODIFIED, null, null, meta); } $this.memcached.get(key + ':headers', function (err, headers) { $this.memcached.get(key + ':body', function (err, body) { next(err, headers, body, meta); }); }); }); } });
var Server = ks_utils.Class({ // Service accepts these options: default_options: { // Boolean switch to control logging. logging: true, // Port on which the HTTP service will listen. port: 9000, // Template used to resolve incoming URL paths to document source URLs. document_url_template: "http://localhost:9001/docs/{path}?raw=1", // Template used to resolve macro names to URLs for the loader. template_url_template: "http://localhost:9001/templates/{name}?raw=1", // Root dir (relative) from which to load macros for the loader. template_root_dir: "macros", // Default cache-control header to send with HTTP loader template_cache_control: "max-age=3600", // Prefix used in headers intended for env variables env_header_prefix: 'x-kumascript-env-', // Max number of loader retries. loader_max_retries: 5, // Time to wait between loader retries. loader_retry_wait: 100, // Max number of macro workers numWorkers: 16, // Max number of concurrent macro processing jobs per request. workerConcurrency: 4, // Max number of jobs handled by a worker before exit workerMaxJobs: 64, // Number of milliseconds to wait before considering a macro timed out workerTimeout: 1000 * 60, // Number of times to retry a macro workerRetries: 3 }, // Build the service, but do not listen yet. initialize: function (options) { var $this = this, app = this.app = express(); this.req_cnt = 0; this.statsd = ks_utils.getStatsD(this.options); // Build the root URL of the document API service. this.doc_base_url = ks_utils.get_base_url( this.options.document_url_template ); if (this.options.memcache) { var mo = this.options.memcache; this.memcached = new Memcached(mo.server, mo.options || {}); } else { // If the configuration is missing, use the fake stub cache this.memcached = new ks_utils.FakeMemcached(); } if (this.options.macro_processor) { this.macro_processor = this.options.macro_processor; } else { var mp_options = _.extend(_.clone(this.options), { autorequire: this.options.autorequire, memcache: this.options.memcache, statsd: this.options.statsd, loader: { module: __dirname + '/loaders', class_name: 'FileLoader', options: { memcache: this.options.memcache, statsd_conf: this.options.statsd_conf, url_template: this.options.template_url_template, cache_control: this.options.template_cache_control, root_dir: this.options.template_root_dir, max_retries: this.options.loader_max_retries, retry_wait: this.options.loader_retry_wait } } }); this.macro_processor = new ks_macros.MacroProcessor(mp_options); } if (!this.macro_processor.options.doc_base_url) { this.macro_processor.options.doc_base_url = this.doc_base_url; } $this.macro_processor.startup(function () { // Configure the HTTP server... if ($this.options.logging) { // Configure a logger that pipes to the winston logger. var logger = morgan( ':remote-addr - - [:date] ":method :url ' + 'HTTP/:http-version" :status :res[content-length] ' + '":referrer" ":user-agent" :response-time', { stream: { write: function(s) { log.info(s.trim(), { source: "server", pid: process.pid }); } } } ); app.use(logger); } app.use(firelogger()); // Set up HTTP routing, pretty simple so far... app.get('/', _.bind($this.root_GET, $this)); app.get('/docs/*', _.bind($this.docs_GET, $this)); app.post('/docs/', _.bind($this.docs_POST, $this)); app.get('/macros/?', _.bind($this.macros_list_GET, $this)); app.get('/healthz/?', _.bind($this.liveness_GET, $this)); app.get('/readiness/?', _.bind($this.readiness_GET, $this)); app.get('/revision/?', _.bind($this.revision_GET, $this)); }); }, // Start the service listening listen: function (port) { port = port || this.options.port; this.server = this.app.listen(port); }, // Close down the service close: function () { var $this = this; $this.macro_processor.shutdown(function () { if ($this.server) { $this.server.close(); } }); }, // #### GET / // // Return something root_GET: function (req, res) { res.send('<html><body><p>Hello from KumaScript!</p></body></html>'); }, // #### GET /docs/* // // Process source documents, respond with the result of macro evaluation. docs_GET: function (req, res) { var $this = this, opts = {}; // Vary caching on values of env vars, as well as X-FireLogger var pfx = this.options.env_header_prefix; var vary = _.chain(req.headers).keys().filter(function (key) { return 0 === key.indexOf(pfx); }).value(); vary.push('X-FireLogger'); res.set('Vary', vary.join(',')); // Create a response cache instance var cache = new ks_caching.ResponseCache({ memcache: this.options.memcache, statsd: $this.statsd, }); cache.cacheResponse(req, res, opts, function (req, res) { var path = req.params[0], url_tmpl = $this.options.document_url_template, doc_url = ks_utils.tmpl(url_tmpl, {path: encodeURI(path)}); var req_opts = { memcached: $this.memcached, statsd: $this.statsd, timeout: $this.options.cache_timeout || 3600, cache_control: req.get('cache-control'), url: doc_url, }; ks_caching.request(req_opts, function (err, resp, src) { if (err) { res.log.error('Problem fetching source document: ' + err.message, { name: 'kumascript', template: '%s: %s', args: [err.name, err.message, err.options] }); res.send(''); } else { $this._evalMacros(src, req, res); } }); }); }, // #### POST /docs/ // // Process POST body, respond with result of macro evaluation docs_POST: function (req, res) { var $this = this, buf = ''; // TODO: Be more flexible with encodings. req.setEncoding('utf8'); req.on('data', function (chunk) { buf += chunk; }); req.on('end', function () { try { var src = buf.length ? buf : ''; $this._evalMacros(src, req, res); } catch (err){ // TODO: Handle errors more gracefully $this._evalMacros('', req, res); } }); }, // #### GET /macros // // Get JSON of available macros (also known as templates) macros_list_GET: function (req, res) { var loader = this.macro_processor.makeLoader(), loader_name = this.macro_processor.options.loader.class_name, data = loader.macros_data(); data.loader = loader_name; res.json(data); }, /** * A "liveness" endpoint for use by Kubernetes or other * similar systems. A successful response from this endpoint * simply proves that this Express app is up and running. It * doesn't mean that its supporting services (like the macro * loader and the document service) can be successfully used * from this service. */ liveness_GET: function (req, res) { res.sendStatus(204); }, /** * A "readiness" endpoint for use by Kubernetes or other * similar systems. A successful response from this endpoint goes * a step further and means not only that this Express app is up * and running, but also that one or more macros have been found * and that the document service is ready. */ readiness_GET: function (req, res) { var msg = 'service unavailable ', kumaReadiness = url.resolve(this.doc_base_url, 'readiness'); // First, check that we can load some macros. try { // If there are no macros or duplicate macros, an error // will be thrown. this.macro_processor.makeLoader(); } catch(err) { msg += `(macro loader error) (${err})`; res.status(503).send(msg); return; } // Finally, check that the document service is ready. request.get(kumaReadiness, function (err, resp, body) { if (!err && ((resp.statusCode >= 200) && (resp.statusCode < 400))) { res.sendStatus(204); } else { var reason = err ? err : body; msg += `(document service is not ready) (${reason})`; res.status(503).send(msg); } }); }, // #### GET /revision // // Return the value of the git commit hash for HEAD. revision_GET: function (req, res) { res.set({'content-type': 'text/plain; charset=utf-8'}) .send(process.env.REVISION_HASH || "undefined"); }, // #### _evalMacros() // // Shared macro evaluator used by GET and POST _evalMacros: function (src, req, res) { try { // Extract env vars from request headers var pfx = this.options.env_header_prefix; var env = _.chain(req.headers).map(function (val, key) { try { if (0 !== key.indexOf(pfx)) { return; } var d_key = key.substr(pfx.length), d_json = (new Buffer(val, 'base64')) .toString('utf-8'), data = JSON.parse(d_json); return [d_key, data]; } catch (e) { // No-op, ignore parsing errors for env vars } }).object().value(); var cc = req.get('cache-control') || ''; env.cache_control = cc; if (cc.indexOf('no-cache') != -1) { env.revalidate_at = (new Date()).getTime(); } // Add a clone of the interactive-examples settings to "env". env.interactive_examples = ks_conf.nconf.get( 'interactive_examples' ); var ctx = { env: env, log: res.log }; // Process the macros... this.macro_processor.process(src, ctx, function (errors, result) { if (errors) { errors.forEach(function (error) { delete error.options.src; res.log.error(error.message, { name: 'kumascript', template: '%s: %s', args: [error.name, error.message, error.options] }); }); } res.send(result); }); } catch (error) { res.log.error(error.message, { name: 'kumascript', template: '%s: %s', args: [error.name, error.message, error.options] }); // HACK: If all else fails, send back the source res.send(src); } } });
var List = module.exports = Class({ // コンストラクタ initialize : function(){ this.list_ = new LinkedList.Manager(); }, // 後始末 finalize : function(){ this.list_ = null; }, push : function(o){ var n = new LinkedList.Node(); n.data = o; this.list_.pushTail(n); }, unshift : function(o){ var n = new LinkedList.Node(); n.data = o; this.list_.pushHead(n); }, pop : function(){ var data = null; if(this.list_.size() > 0){ var n = this.list_.popTail(); data = n.data; n.data = null; } return data; }, shift : function(){ var data = null; if(this.list_.size() > 0){ var n = this.list_.popHead(); data = n.data; n.data = null; } return data; }, // キューをクリアする clear : function(){ this.list_.finalize(); }, // キューの長さを取得する size : function(){ return this.list_.size(); }, // キューをスキャンする scan : function(callback){ var i = 0; this.list_.scanHead(function(node){ if(!callback(i++,node.data)){ return false; } return true; }); return true; }, });
var MacroProcessor = ks_utils.Class(EventEmitter, { // #### Default options default_options: { loader: { module: __dirname + '/loaders', class_name: 'HTTPLoader', options: { filename_template: __dirname + '/fixtures/templates/{name}.ejs' } }, memcache: null, // Max number of macro workers numWorkers: 16, // Max number of concurrent macro processing jobs per request. workerConcurrency: 4, // Max number of jobs handled by a worker before exit workerMaxJobs: 64, // Number of milliseconds to wait before considering a macro timed out workerTimeout: 1000 * 60 * 10, // Number of times to retry a failed macro workerRetries: 3 }, initialize: function (options) { this.statsd = ks_utils.getStatsD(this.options); }, startup: function (next) { var $this = this; var pool = this.worker_pool = new hirelings.Pool({ module: __dirname + '/macros-worker.js', max_processes: this.options.numWorkers, max_jobs_per_process: this.options.workerMaxJobs, retries: this.options.workerRetries, options: { autorequire: this.options.autorequire, loader: this.options.loader, memcache: this.options.memcache } }); // HACK: Maybe hirelings should support StatsD directly ['spawn', 'exit', 'task', 'backlog', 'drain'] .forEach(function (name) { pool.on(name, function () { $this.statsd.increment('workers.pool.' + name); $this.measureWorkers(); }); }); return next(); }, shutdown: function (next) { if (this.worker_pool) { this.worker_pool.exit(); } return next(); }, // #### Process macros in content process: function (src, ctx, process_done) { var $this = this; var errors = []; var macros = {}; // Common exit point for processing var t_process_start = t_now(); function _done(errors, src) { $this.statsd.timing('macros.t_processing', t_now() - t_process_start); if (!errors.length) errors = null; return process_done(errors, src); } // Attempt to parse the document, trap errors var tokens = []; try { tokens = ks_parser.parse(src); } catch (e) { errors.push(new ks_errors.DocumentParsingError({ error: e, src: src })); return _done(errors, src); } var autorequire = $this.options.autorequire; var templates_to_reload = []; if (ctx.env && 'no-cache' == ctx.env.cache_control) { // Extract a unique list of template names used in macros. var template_names = _.chain(tokens) .filter(function (tok) { return 'MACRO' == tok.type; }) .map(function (tok) { return tok.name; }) .uniq().value(); // Templates to flush include those used in macros and // autorequired modules templates_to_reload = _.union(template_names, _.values(autorequire)); } // Intercept the logger from context, if present. var log = null; if ('log' in ctx) { log = ctx.log; } // Macro processing queue managing process of sending jobs to the // evaluation cluster. // // Yes, this is an (internal) queue managing submissions to another // (external) queue. But, it limits the number of concurrent jobs per // document, and tells us when this document's macros are done. var macro_q = async.queue(function (hash, q_next) { var t_enqueued = t_now(); var t_started = null; var token = macros[hash]; var work = {token: token, src: src, ctx: ctx}; var job = $this.worker_pool.enqueue(work, function (err, rv) { if (err) { errors.push(new ks_errors.TemplateExecutionError({ error: err, stack: '', name: token.name, src: src, token: token })); token.out = '{{ ' + token.name + ' }}'; } else if (rv.error) { var err_cls = rv.error[0]; var err_opts = rv.error[1]; errors.push(new ks_errors[err_cls](err_opts)); token.out = '{{ ' + token.name + ' }}'; } else { token.out = rv.result; } var t_running = t_now() - t_started; $this.statsd.timing('macros.t_running.overall', t_running); $this.statsd.timing('macros.t_running.by_name.' + token.name, t_running); $this.statsd.timing('macros.t_enqueued', t_now() - t_enqueued); $this.statsd.increment('macros.by_name.' + token.name); q_next(); }); job.on('start', function () { t_started = t_now(); }); job.on('progress', function (msg) { var type = msg.type, args = msg.args; switch (type) { case "log": log[args[0]](args[1]); break; case "statsd": $this.statsd[args[0]](args[1]); break; } }); job.on('retry', function () { $this.statsd.increment('macros.retries.overall'); $this.statsd.increment('macros.retries.' + token.name); }); }, $this.options.workerConcurrency); // Before queueing macros for processing, reload templates (if any) $this.reloadTemplates(templates_to_reload, function (err) { // Scan through the tokens, queue unique macros for evaluation. tokens.forEach(function (token) { if ('MACRO' == token.type) { token.hash = $this.hashTokenArgs(token); if (!(token.hash in macros)) { macros[token.hash] = token; macro_q.push(token.hash); } } }); // Exit point when the processing queue has drained. macro_q.drain = function (err) { // Assemble output text by gluing together text tokens and the // results of macro evaluation. var src_out = _.map(tokens, function (token) { if ('TEXT' == token.type) { return token.chars; } else if ('MACRO' == token.type) { return macros[token.hash].out; } }).join(''); return _done(errors, src_out); } // If no macros were queued up, jump straight to drain. if (0 == macro_q.length()) { macro_q.drain(); } }); }, // #### Produce a unique hash for macro // A macro's unique hash encompasses the template name and the arguments hashTokenArgs: function (token) { // Hash the macro name and args, to identify unique calls. var hash = crypto.createHash('md5').update(token.name); if (token.args.length > 0) { // Update the hash with arguments, if any... if (_.isObject(token.args[0])) { // JSON-style args, so stringify the object. hash.update(JSON.stringify(token.args)); } else { // Otherwise, this is a simple string list. hash.update(token.args.join(',')); } } return hash.digest('hex'); }, // #### Force-reload the named templates, if any. reloadTemplates: function (names, done) { if (0 == names.length) { return done(); } try { var loader_module = require(this.options.loader.module); var loader_class = loader_module[this.options.loader.class_name]; var loader_options = _.clone(this.options.loader.options); // Use a Cache-Control header that forces a fresh cache. loader_options.cache_control = 'no-cache'; loader_options.statsd = this.statsd; var loader = new loader_class(loader_options); async.forEach(names, function (name, e_next) { loader.get(name, e_next); }, done); } catch (e) { done(e); } }, // #### Record some measurements from the worker pool. measureWorkers: function () { var stats = this.worker_pool.getStats(); this.statsd.gauge('workers.total', stats.workers); this.statsd.gauge('workers.busy', stats.busy); this.statsd.gauge('workers.backlog', stats.backlog); } });
fibers = require('fibers'), Future = require('fibers/future'), wait = Future.wait, ks_utils = require(__dirname + '/utils'); // ### BaseTemplate // // The base template script class var BaseTemplate = ks_utils.Class({ default_options: { // Templates compile from textual source source: '' }, // #### execute // // Execute the template with the given arguments. The callback should expect // `(err, result)` parameters. execute: function (args, ctx, next) { next(null, "UNIMPLEMENTED"); } }); // ### JSTemplate // // Template executed using sandboxed JS var JSTemplate = ks_utils.Class(BaseTemplate, { initialize: function (options) { BaseTemplate.prototype.initialize.apply(this, arguments); var vm = require("vm");
var Job = Class({ // コンストラクタ initialize : function( interval, // 実行間隔 next, // 処理開始時間 callback // 処理関数 ){ this.interval_ = interval; this.next_ = next; this.callback_ = callback; }, // 後始末 finalize : function(){ this.interval_ = 0; this.next_ = 0; this.callback_ = null; }, // 次の仕事をスケジューリングする next : function(){ this.next_ += this.interval_; }, // スケジューリングされている時間を経過したかチェックする isExec : function(now){ if(now >= this.next_){ return true; } return false; }, // 処理を実行する exec : function(now){ if(this.callback_){ this.callback_(now); } }, });
var HashMap = module.exports = Class({ /** * コンストラクタ */ initialize : function(){ this.hash_ = {}; this.size_ = 0; }, /** * サイズの取得 * @returns {Number} 値 */ size : function(){ return this.size_; }, /** * キーから値を取得 * @param {String} key キー * @returns {Valiant} 値 */ get : function(key){ assert(this.isExist(key)); return this.hash_[key]; }, /** * キーで値を設定 * @param {String} key キー * @param {Valiant} value 値 */ set : function(key,value){ if(this.hash_[key] === undefined){ ++this.size_; } this.hash_[key] = value; }, /** * キーが存在するかどうか * @param {String} key キー * @returns {Boolean} 存在可否 */ isExist : function(key){ return (this.hash_[key] !== undefined); }, /** * キーを削除 * @param {String} key キー */ remove : function(key){ assert(this.isExist(key)); --this.size_; assert(this.size_ >= 0); delete this.hash_[key]; }, /** * スキャン関数 * 処理関数にfalseを返させると途中で処理を終了する * @param {Function} callback 処理関数function(key,value){return true;} */ scan : function(callback){ var w = this.hash_; for(var key in w) if(w.hasOwnProperty(key)){ if(!callback(key, w[key])){ return false; } } return true; }, });