├── .gitignore ├── .travis.yml ├── test ├── proper-exit.tap.js ├── async-context.tap.js ├── async-no-run-queue-multiple.tap.js ├── zlib.tap.js ├── namespaces.tap.js ├── run-and-return.tap.js ├── simple.tap.js ├── bind.tap.js ├── net-events.tap.js ├── interleave-contexts.tap.js ├── monkeypatching.tap.js ├── timers.tap.js ├── nesting.tap.js ├── crypto.tap.js ├── promises.tap.js ├── error-handling.tap.js ├── dns.tap.js ├── tracer-scenarios.tap.js ├── bind-emitter.tap.js └── fs.tap.js ├── .eslintrc ├── package.json ├── LICENSE ├── CHANGELOG.md ├── context.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: 2 | - "npm test" 3 | 4 | language: node_js 5 | 6 | node_js: 7 | - "0.10" 8 | - "0.12" 9 | - "4" 10 | - "6" 11 | - "8" 12 | - "9" 13 | 14 | sudo: false 15 | -------------------------------------------------------------------------------- /test/proper-exit.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // I love when a tap.plan() comes together 4 | console.log('1..1'); 5 | 6 | process.on('uncaughtException', function (err) { 7 | if (err.message === 'oops') { 8 | console.log("ok got expected message: %s", err.message); 9 | } 10 | else { 11 | throw err; 12 | } 13 | }); 14 | 15 | var cls = require('../context.js'); 16 | var ns = cls.createNamespace('x'); 17 | ns.run(function () { throw new Error('oops'); }); 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env" : { 3 | "node" : true 4 | }, 5 | "rules" : { 6 | "curly" : 0, 7 | "no-lonely-if" : 1, 8 | "no-mixed-requires" : 0, 9 | "no-underscore-dangle" : 0, 10 | "no-unused-vars" : [2, {"vars" : "all", "args" : "after-used"}], 11 | "no-use-before-define" : [2, "nofunc"], 12 | "quotes" : 0, 13 | "semi" : [2, "always"], 14 | "space-after-keywords" : 1, 15 | "space-infix-ops" : 0, 16 | "strict" : 0, 17 | "max-len" : [1, 90, 2] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/async-context.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap') 4 | , test = tap.test 5 | , createNamespace = require('../context.js').createNamespace 6 | ; 7 | 8 | test("asynchronously propagating state with local-context-domains", function (t) { 9 | t.plan(2); 10 | 11 | var namespace = createNamespace('namespace'); 12 | t.ok(process.namespaces.namespace, "namespace has been created"); 13 | 14 | namespace.run(function () { 15 | namespace.set('test', 1337); 16 | t.equal(namespace.get('test'), 1337, "namespace is working"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/async-no-run-queue-multiple.tap.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | , cls = require('../context.js') 3 | ; 4 | 5 | test("minimized test case that caused #6011 patch to fail", function (t) { 6 | t.plan(3); 7 | 8 | console.log('+'); 9 | // when the flaw was in the patch, commenting out this line would fix things: 10 | process.nextTick(function () { console.log('!'); }); 11 | 12 | var n = cls.createNamespace("test"); 13 | t.ok(!n.get('state'), "state should not yet be visible"); 14 | 15 | n.run(function () { 16 | n.set('state', true); 17 | t.ok(n.get('state'), "state should be visible"); 18 | 19 | process.nextTick(function () { 20 | t.ok(n.get('state'), "state should be visible"); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/zlib.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap') 4 | , test = tap.test 5 | , createNamespace = require('../context.js').createNamespace 6 | ; 7 | 8 | var zlib = require('zlib'); 9 | 10 | test("continuation-local state with zlib", function (t) { 11 | t.plan(1); 12 | 13 | var namespace = createNamespace('namespace'); 14 | namespace.run(function () { 15 | namespace.set('test', 0xabad1dea); 16 | 17 | t.test("deflate", function (t) { 18 | namespace.run(function () { 19 | namespace.set('test', 42); 20 | zlib.deflate(new Buffer("Goodbye World"), function (err) { 21 | if (err) throw err; 22 | t.equal(namespace.get('test'), 42, "mutated state was preserved"); 23 | t.end(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "continuation-local-storage", 3 | "version": "3.2.1", 4 | "description": "userland implementation of https://github.com/joyent/node/issues/5243", 5 | "main": "context.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "tap test/*.tap.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/othiym23/node-continuation-local-storage.git" 15 | }, 16 | "keywords": [ 17 | "threading", 18 | "shared", 19 | "context", 20 | "domains", 21 | "tracing", 22 | "logging" 23 | ], 24 | "author": "Forrest L Norvell ", 25 | "contributors": [ 26 | "Tim Caswell ", 27 | "Forrest L Norvell " 28 | ], 29 | "license": "BSD-2-Clause", 30 | "devDependencies": { 31 | "semver": "^5.4.1", 32 | "tap": "^10.3.1" 33 | }, 34 | "dependencies": { 35 | "async-listener": "^0.6.0", 36 | "emitter-listener": "^1.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/namespaces.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap'); 4 | var test = tap.test; 5 | 6 | var context = require('../context.js'); 7 | 8 | test("namespace management", function (t) { 9 | t.plan(8); 10 | 11 | t.throws(function () { context.createNamespace(); }, "name is required"); 12 | 13 | var namespace = context.createNamespace('test'); 14 | t.ok(namespace, "namespace is returned upon creation"); 15 | 16 | t.equal(context.getNamespace('test'), namespace, "namespace lookup works"); 17 | 18 | t.doesNotThrow(function () { context.reset(); }, "allows resetting namespaces"); 19 | 20 | t.equal(Object.keys(process.namespaces).length, 0, "namespaces have been reset"); 21 | 22 | namespace = context.createNamespace('another'); 23 | t.ok(process.namespaces.another, "namespace is available from global"); 24 | 25 | t.doesNotThrow(function () { context.destroyNamespace('another'); }, 26 | "destroying works"); 27 | 28 | t.notOk(process.namespaces.another, "namespace has been removed"); 29 | }); 30 | -------------------------------------------------------------------------------- /test/run-and-return.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // stdlib 4 | var tap = require('tap'); 5 | var test = tap.test; 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | // module under test 9 | var context = require('../context.js'); 10 | 11 | // multiple contexts in use 12 | var tracer = context.createNamespace('tracer'); 13 | 14 | 15 | test("simple tracer built on contexts", function (t) { 16 | t.plan(7); 17 | 18 | var harvester = new EventEmitter(); 19 | 20 | harvester.on('finished', function (transaction) { 21 | t.ok(transaction, "transaction should have been passed in"); 22 | t.equal(transaction.status, 'ok', "transaction should have finished OK"); 23 | t.equal(Object.keys(process.namespaces).length, 1, "Should only have one namespace."); 24 | }); 25 | 26 | var returnValue = {}; 27 | 28 | var returnedValue = tracer.runAndReturn(function(context) { 29 | t.ok(tracer.active, "tracer should have an active context"); 30 | tracer.set('transaction', {status : 'ok'}); 31 | t.ok(tracer.get('transaction'), "can retrieve newly-set value"); 32 | t.equal(tracer.get('transaction').status, 'ok', "value should be correct"); 33 | 34 | harvester.emit('finished', context.transaction); 35 | 36 | return returnValue; 37 | }); 38 | 39 | t.equal(returnedValue, returnValue, "method should pass through return value of function run in scope"); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016, Forrest L Norvell 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /test/simple.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // stdlib 4 | var tap = require('tap'); 5 | var test = tap.test; 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | // module under test 9 | var context = require('../context.js'); 10 | 11 | // multiple contexts in use 12 | var tracer = context.createNamespace('tracer'); 13 | 14 | function Trace(harvester) { 15 | this.harvester = harvester; 16 | } 17 | 18 | Trace.prototype.runHandler = function (handler) { 19 | var trace = tracer.run(handler); 20 | this.harvester.emit('finished', trace.transaction); 21 | }; 22 | 23 | 24 | test("simple tracer built on contexts", function (t) { 25 | t.plan(6); 26 | 27 | var harvester = new EventEmitter(); 28 | var trace = new Trace(harvester); 29 | 30 | harvester.on('finished', function (transaction) { 31 | t.ok(transaction, "transaction should have been passed in"); 32 | t.equal(transaction.status, 'ok', "transaction should have finished OK"); 33 | t.equal(Object.keys(process.namespaces).length, 1, "Should only have one namespace."); 34 | }); 35 | 36 | trace.runHandler(function inScope() { 37 | t.ok(tracer.active, "tracer should have an active context"); 38 | tracer.set('transaction', {status : 'ok'}); 39 | t.ok(tracer.get('transaction'), "can retrieve newly-set value"); 40 | t.equal(tracer.get('transaction').status, 'ok', "value should be correct"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/bind.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // stdlib 4 | var tap = require('tap'); 5 | var test = tap.test; 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | // module under test 9 | var context = require('../context.js'); 10 | 11 | // multiple contexts in use 12 | var tracer = context.createNamespace('tracer'); 13 | 14 | function Trace(harvester) { 15 | this.harvester = harvester; 16 | } 17 | 18 | Trace.prototype.runHandler = function (callback) { 19 | var wrapped = tracer.bind(function () { 20 | callback(); 21 | this.harvester.emit('finished', tracer.get('transaction')); 22 | }.bind(this)); 23 | wrapped(); 24 | }; 25 | 26 | 27 | test("simple tracer built on contexts", function (t) { 28 | t.plan(6); 29 | 30 | var harvester = new EventEmitter(); 31 | var trace = new Trace(harvester); 32 | 33 | harvester.on('finished', function (transaction) { 34 | t.ok(transaction, "transaction should have been passed in"); 35 | t.equal(transaction.status, 'ok', "transaction should have finished OK"); 36 | t.equal(Object.keys(process.namespaces).length, 1, "Should only have one namespace."); 37 | }); 38 | 39 | trace.runHandler(function inScope() { 40 | t.ok(tracer.active, "tracer should have an active context"); 41 | tracer.set('transaction', {status : 'ok'}); 42 | t.ok(tracer.get('transaction'), "can retrieve newly-set value"); 43 | t.equal(tracer.get('transaction').status, 'ok', "value should be correct"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/net-events.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net') 4 | , tap = require('tap') 5 | , test = tap.test 6 | , createNamespace = require('../context').createNamespace 7 | ; 8 | 9 | test("continuation-local state with net connection", function (t) { 10 | t.plan(4); 11 | 12 | var namespace = createNamespace('net'); 13 | namespace.run(function () { 14 | namespace.set('test', 0xabad1dea); 15 | 16 | var server; 17 | namespace.run(function () { 18 | namespace.set('test', 0x1337); 19 | 20 | server = net.createServer(function (socket) { 21 | t.equal(namespace.get('test'), 0x1337, "state has been mutated"); 22 | socket.on("data", function () { 23 | t.equal(namespace.get('test'), 0x1337, "state is still preserved"); 24 | server.close(); 25 | socket.end("GoodBye"); 26 | }); 27 | }); 28 | server.listen(function () { 29 | var address = server.address(); 30 | namespace.run(function () { 31 | namespace.set("test", "MONKEY"); 32 | var client = net.connect(address.port, function () { 33 | t.equal(namespace.get("test"), "MONKEY", 34 | "state preserved for client connection"); 35 | client.write("Hello"); 36 | client.on("data", function () { 37 | t.equal(namespace.get("test"), "MONKEY", "state preserved for client data"); 38 | t.end(); 39 | }); 40 | }); 41 | }); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/interleave-contexts.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cls = require('../context.js') 4 | , test = require('tap').test 5 | ; 6 | 7 | function cleanNamespace(name){ 8 | if (cls.getNamespace(name)) cls.destroyNamespace(name); 9 | return cls.createNamespace(name); 10 | } 11 | 12 | test("interleaved contexts", function (t) { 13 | t.plan(3); 14 | 15 | t.test("interleaving with run", function (t) { 16 | t.plan(2); 17 | 18 | var ns = cleanNamespace('test'); 19 | 20 | var ctx = ns.createContext(); 21 | 22 | ns.enter(ctx); 23 | ns.run(function () { 24 | t.equal(ns._set.length, 2, "2 contexts in the active set"); 25 | t.doesNotThrow(function () { ns.exit(ctx); }); 26 | }); 27 | }); 28 | 29 | t.test("entering and exiting staggered", function (t) { 30 | t.plan(4); 31 | 32 | var ns = cleanNamespace('test'); 33 | 34 | var ctx1 = ns.createContext(); 35 | var ctx2 = ns.createContext(); 36 | 37 | t.doesNotThrow(function () { ns.enter(ctx1); }); 38 | t.doesNotThrow(function () { ns.enter(ctx2); }); 39 | 40 | t.doesNotThrow(function () { ns.exit(ctx1); }); 41 | t.doesNotThrow(function () { ns.exit(ctx2); }); 42 | }); 43 | 44 | t.test("creating, entering and exiting staggered", function (t) { 45 | t.plan(4); 46 | 47 | var ns = cleanNamespace('test'); 48 | 49 | var ctx1 = ns.createContext(); 50 | t.doesNotThrow(function () { ns.enter(ctx1); }); 51 | 52 | var ctx2 = ns.createContext(); 53 | t.doesNotThrow(function () { ns.enter(ctx2); }); 54 | 55 | t.doesNotThrow(function () { ns.exit(ctx1); }); 56 | t.doesNotThrow(function () { ns.exit(ctx2); }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/monkeypatching.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | 4 | if (!process.addAsyncListener) { 5 | test("overwriting startup.processNextTick", function (t) { 6 | t.plan(2); 7 | 8 | t.doesNotThrow(function () { require('../context.js'); }); 9 | 10 | t.ok(process.nextTick.__wrapped, "should wrap process.nextTick()"); 11 | }); 12 | 13 | test("overwriting domain helpers", function (t) { 14 | // domain helpers were only in 0.10.x 15 | if (!(process._nextDomainTick && process._tickDomainCallback)) { 16 | return t.end(); 17 | } 18 | 19 | t.plan(2); 20 | 21 | t.ok(process._nextDomainTick.__wrapped, 22 | "should wrap process._nextDomainTick()"); 23 | t.ok(process._tickDomainCallback.__wrapped, 24 | "should wrap process._tickDomainCallback()"); 25 | }); 26 | 27 | test("overwriting timers", function (t) { 28 | t.plan(2); 29 | 30 | var timers = require('timers'); 31 | t.ok(timers.setTimeout.__wrapped, "should wrap setTimeout()"); 32 | t.ok(timers.setInterval.__wrapped, "should wrap setInterval()"); 33 | 34 | /* It would be nice to test that monkeypatching preserves the status quo 35 | * ante, but assert thinks setTimeout !== global.setTimeout (why?) and both of 36 | * those are a wrapper around NativeModule.require("timers").setTimeout, 37 | * presumably to try to prevent the kind of "fun" I'm having here. 38 | */ 39 | }); 40 | 41 | test("overwriting setImmediate", function (t) { 42 | // setTimeout's a johnny-come-lately 43 | if (!global.setImmediate) return t.end(); 44 | 45 | t.plan(1); 46 | 47 | t.ok(require('timers').setImmediate.__wrapped, "should wrap setImmediate()"); 48 | 49 | /* It would be nice to test that monkeypatching preserves the status quo 50 | * ante, but assert thinks setTimeout !== global.setTimeout (why?) and both of 51 | * those are a wrapper around NativeModule.require("timers").setTimeout, 52 | * presumably to try to prevent the kind of "fun" I'm having here. 53 | */ 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /test/timers.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap') 4 | , test = tap.test 5 | , createNamespace = require('../context.js').createNamespace 6 | ; 7 | 8 | test("continuation-local state with timers", function (t) { 9 | t.plan(4); 10 | 11 | var namespace = createNamespace('namespace'); 12 | namespace.run(function () { 13 | namespace.set('test', 0xabad1dea); 14 | 15 | t.test("process.nextTick", function (t) { 16 | namespace.run(function () { 17 | namespace.set('test', 31337); 18 | t.equal(namespace.get('test'), 31337, "state has been mutated"); 19 | 20 | process.nextTick(function () { 21 | t.equal(namespace.get('test'), 31337, 22 | "mutated state has persisted to process.nextTick's callback"); 23 | 24 | t.end(); 25 | }); 26 | }); 27 | }); 28 | 29 | t.test("setImmediate", function (t) { 30 | // setImmediate only in Node > 0.9.x 31 | if (!global.setImmediate) return t.end(); 32 | 33 | namespace.run(function () { 34 | namespace.set('test', 999); 35 | t.equal(namespace.get('test'), 999, "state has been mutated"); 36 | 37 | setImmediate(function () { 38 | t.equal(namespace.get('test'), 999, 39 | "mutated state has persisted to setImmediate's callback"); 40 | 41 | t.end(); 42 | }); 43 | }); 44 | }); 45 | 46 | t.test("setTimeout", function (t) { 47 | namespace.run(function () { 48 | namespace.set('test', 54321); 49 | t.equal(namespace.get('test'), 54321, "state has been mutated"); 50 | 51 | setTimeout(function () { 52 | t.equal(namespace.get('test'), 54321, 53 | "mutated state has persisted to setTimeout's callback"); 54 | 55 | t.end(); 56 | }); 57 | }); 58 | }); 59 | 60 | t.test("setInterval", function (t) { 61 | namespace.run(function () { 62 | namespace.set('test', 10101); 63 | t.equal(namespace.get('test'), 10101, 64 | "continuation-local state has been mutated"); 65 | 66 | var ref = setInterval(function () { 67 | t.equal(namespace.get('test'), 10101, 68 | "mutated state has persisted to setInterval's callback"); 69 | 70 | clearInterval(ref); 71 | t.end(); 72 | }, 20); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/nesting.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap'); 4 | var test = tap.test; 5 | 6 | var cls = require('../context.js'); 7 | 8 | test("nested contexts on a single namespace", function (t) { 9 | t.plan(7); 10 | 11 | var namespace = cls.createNamespace("namespace"); 12 | namespace.run(function () { 13 | namespace.set("value", 1); 14 | 15 | t.equal(namespace.get("value"), 1, 16 | "namespaces have associated data even without contexts."); 17 | 18 | namespace.run(function () { 19 | t.equal(namespace.get("value"), 1, "lookup will check enclosing context"); 20 | namespace.set("value", 2); 21 | t.equal(namespace.get("value"), 2, "setting works on top-level context"); 22 | 23 | namespace.run(function () { 24 | t.equal(namespace.get("value"), 2, "lookup will check enclosing context"); 25 | namespace.set("value", 3); 26 | t.equal(namespace.get("value"), 3, "setting works on nested context"); 27 | }); 28 | 29 | t.equal(namespace.get("value"), 2, 30 | "should revert to value set in top-level context"); 31 | }); 32 | 33 | t.equal(namespace.get("value"), 1, "namespace retains its outermost value."); 34 | }); 35 | }); 36 | 37 | test("the example from the docs", function (t) { 38 | var writer = cls.createNamespace('writer'); 39 | writer.run(function () { 40 | writer.set('value', 0); 41 | 42 | t.equal(writer.get('value'), 0, "outer hasn't been entered yet"); 43 | function requestHandler() { 44 | writer.run(function (outer) { 45 | t.equal(writer.active, outer, "writer.active == outer"); 46 | 47 | writer.set('value', 1); 48 | t.equal(writer.get('value'), 1, "writer.active == outer"); 49 | t.equal(outer.value, 1, "outer is active"); 50 | 51 | process.nextTick(function () { 52 | t.equal(writer.active, outer, "writer.active == outer"); 53 | t.equal(writer.get('value'), 1, "inner has been entered"); 54 | writer.run(function (inner) { 55 | t.equal(writer.active, inner, "writer.active == inner"); 56 | 57 | writer.set('value', 2); 58 | t.equal(outer.value, 1, "outer is unchanged"); 59 | t.equal(inner.value, 2, "inner is active"); 60 | t.equal(writer.get('value'), 2, "writer.active == inner"); 61 | }); 62 | }); 63 | }); 64 | 65 | setTimeout(function () { 66 | t.equal(writer.get('value'), 0, "writer.active == global"); 67 | t.end(); 68 | }, 100); 69 | } 70 | 71 | requestHandler(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/crypto.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap') 4 | , semver = require('semver') 5 | , test = tap.test 6 | , createNamespace = require('../context.js').createNamespace 7 | ; 8 | 9 | var crypto; 10 | try { crypto = require('crypto'); } 11 | catch (err) {} 12 | 13 | if (crypto) { 14 | test("continuation-local state with crypto.randomBytes", function (t) { 15 | t.plan(1); 16 | 17 | var namespace = createNamespace('namespace'); 18 | namespace.run(function () { 19 | namespace.set('test', 0xabad1dea); 20 | 21 | t.test("randomBytes", function (t) { 22 | namespace.run(function () { 23 | namespace.set('test', 42); 24 | crypto.randomBytes(100, function (err) { 25 | if (err) throw err; 26 | t.equal(namespace.get('test'), 42, "mutated state was preserved"); 27 | t.end(); 28 | }); 29 | }); 30 | }); 31 | }); 32 | }); 33 | 34 | test("continuation-local state with crypto.pseudoRandomBytes", function (t) { 35 | t.plan(1); 36 | 37 | var namespace = createNamespace('namespace'); 38 | namespace.run(function () { 39 | namespace.set('test', 0xabad1dea); 40 | 41 | t.test("pseudoRandomBytes", function (t) { 42 | namespace.run(function () { 43 | namespace.set('test', 42); 44 | crypto.pseudoRandomBytes(100, function (err) { 45 | if (err) throw err; 46 | t.equal(namespace.get('test'), 42, "mutated state was preserved"); 47 | t.end(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | test("continuation-local state with crypto.pbkdf2", function (t) { 55 | t.plan(1); 56 | 57 | var namespace = createNamespace('namespace'); 58 | namespace.run(function () { 59 | namespace.set('test', 0xabad1dea); 60 | 61 | t.test("pbkdf2", function (t) { 62 | namespace.run(function () { 63 | namespace.set('test', 42); 64 | // this API changed after 0.10.0, and errors if digest is missing after v6 65 | if (semver.gte(process.version, "0.12.0")) { 66 | crypto.pbkdf2("s3cr3tz", "451243", 10, 40, "sha512", function (err) { 67 | if (err) throw err; 68 | t.equal(namespace.get('test'), 42, "mutated state was preserved"); 69 | t.end(); 70 | }); 71 | } else { 72 | crypto.pbkdf2("s3cr3tz", "451243", 10, 40, function (err) { 73 | if (err) throw err; 74 | t.equal(namespace.get('test'), 42, "mutated state was preserved"); 75 | t.end(); 76 | }); 77 | } 78 | }); 79 | }); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /test/promises.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tap = require('tap') 4 | , test = tap.test 5 | , createNamespace = require('../context.js').createNamespace 6 | ; 7 | 8 | test("continuation-local state with promises", function (t) { 9 | t.plan(4); 10 | 11 | var namespace = createNamespace('namespace'); 12 | namespace.run(function () { 13 | namespace.set('test', 0xabad1dea); 14 | 15 | t.test("chained promises", function (t) { 16 | if (!global.Promise) return t.end(); 17 | 18 | namespace.run(function () { 19 | namespace.set('test', 31337); 20 | t.equal(namespace.get('test'), 31337, "state has been mutated"); 21 | 22 | Promise.resolve() 23 | .then(function () { 24 | t.equal(namespace.get('test'), 31337, 25 | "mutated state has persisted to first continuation"); 26 | }) 27 | .then(function () { 28 | t.equal(namespace.get('test'), 31337, 29 | "mutated state has persisted to second continuation"); 30 | }) 31 | .then(function () { 32 | t.equal(namespace.get('test'), 31337, 33 | "mutated state has persisted to third continuation"); 34 | t.end(); 35 | }); 36 | }); 37 | }); 38 | 39 | t.test("chained unwrapped promises", function (t) { 40 | if (!global.Promise) return t.end(); 41 | 42 | namespace.run(function () { 43 | namespace.set('test', 999); 44 | t.equal(namespace.get('test'), 999, "state has been mutated"); 45 | 46 | Promise.resolve() 47 | .then(function () { 48 | t.equal(namespace.get('test'), 999, 49 | "mutated state has persisted to first continuation"); 50 | return Promise.resolve(); 51 | }) 52 | .then(function () { 53 | t.equal(namespace.get('test'), 999, 54 | "mutated state has persisted to second continuation"); 55 | return Promise.resolve(); 56 | }) 57 | .then(function () { 58 | t.equal(namespace.get('test'), 999, 59 | "mutated state has persisted to third continuation"); 60 | t.end(); 61 | }); 62 | }); 63 | }); 64 | 65 | t.test("nested promises", function (t) { 66 | if (!global.Promise) return t.end(); 67 | 68 | namespace.run(function () { 69 | namespace.set('test', 54321); 70 | t.equal(namespace.get('test'), 54321, "state has been mutated"); 71 | 72 | Promise.resolve() 73 | .then(function () { 74 | t.equal(namespace.get('test'), 54321, 75 | "mutated state has persisted to first continuation"); 76 | 77 | Promise.resolve() 78 | .then(function () { 79 | t.equal(namespace.get('test'), 54321, 80 | "mutated state has persisted to second continuation"); 81 | 82 | Promise.resolve() 83 | .then(function () { 84 | t.equal(namespace.get('test'), 54321, 85 | "mutated state has persisted to third continuation"); 86 | t.end(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | t.test("forked continuations", function (t) { 94 | if (!global.Promise) return t.end(); 95 | 96 | namespace.run(function () { 97 | namespace.set('test', 10101); 98 | t.equal(namespace.get('test'), 10101, "state has been mutated"); 99 | 100 | var promise = Promise.resolve(); 101 | 102 | promise 103 | .then(function () { 104 | t.equal(namespace.get('test'), 10101, 105 | "mutated state has persisted to first continuation"); 106 | }); 107 | promise 108 | .then(function () { 109 | t.equal(namespace.get('test'), 10101, 110 | "mutated state has persisted to second continuation"); 111 | }); 112 | promise 113 | .then(function () { 114 | t.equal(namespace.get('test'), 10101, 115 | "mutated state has persisted to third continuation"); 116 | t.end(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/error-handling.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tap').test 4 | , cls = require('../context.js') 5 | , domain = require('domain') 6 | ; 7 | 8 | test("continuation-local storage glue with a throw in the continuation chain", 9 | function (t) { 10 | var namespace = cls.createNamespace('test'); 11 | namespace.run(function () { 12 | var d = domain.create(); 13 | namespace.set('outer', true); 14 | namespace.bindEmitter(d); 15 | 16 | d.on('error', function (blerg) { 17 | t.equal(blerg.message, "explicitly nonlocal exit", "got the expected exception"); 18 | t.ok(namespace.get('outer'), "outer context is still active"); 19 | t.notOk(namespace.get('inner'), "inner context should have been exited by throw"); 20 | t.equal(namespace._set.length, 1, "should be back to outer state"); 21 | 22 | cls.destroyNamespace('test'); 23 | t.end(); 24 | }); 25 | 26 | // tap is only trying to help 27 | process.nextTick(d.bind(function () { 28 | t.ok(namespace.get('outer'), "outer mutation worked"); 29 | t.notOk(namespace.get('inner'), "inner mutation hasn't happened yet"); 30 | 31 | namespace.run(function () { 32 | namespace.set('inner', true); 33 | throw new Error("explicitly nonlocal exit"); 34 | }); 35 | })); 36 | }); 37 | }); 38 | 39 | test("synchronous throw attaches the context", function (t) { 40 | t.plan(3); 41 | 42 | var namespace = cls.createNamespace('cls@synchronous'); 43 | namespace.run(function () { 44 | namespace.set('value', 'transaction clear'); 45 | try { 46 | namespace.run(function () { 47 | namespace.set('value', 'transaction set'); 48 | throw new Error('cls@synchronous explosion'); 49 | }); 50 | } 51 | catch (e) { 52 | t.ok(namespace.fromException(e), "context was attached to error"); 53 | t.equal(namespace.fromException(e)['value'], 'transaction set', 54 | "found the inner value"); 55 | } 56 | 57 | t.equal(namespace.get('value'), 'transaction clear', "everything was reset"); 58 | }); 59 | 60 | cls.destroyNamespace('cls@synchronous'); 61 | }); 62 | 63 | test("synchronous throw checks if error exists", function (t) { 64 | t.plan(2); 65 | 66 | var namespace = cls.createNamespace('cls@synchronous-null-error'); 67 | namespace.run(function () { 68 | namespace.set('value', 'transaction clear'); 69 | try { 70 | namespace.run(function () { 71 | namespace.set('value', 'transaction set'); 72 | throw null; 73 | }); 74 | } 75 | catch (e) { 76 | // as we had a null error, cls couldn't set the new inner value 77 | t.equal(namespace.get('value'), 'transaction clear', 'from outer value'); 78 | } 79 | 80 | t.equal(namespace.get('value'), 'transaction clear', "everything was reset"); 81 | }); 82 | 83 | cls.destroyNamespace('cls@synchronous-null-error'); 84 | }); 85 | 86 | test("throw in process.nextTick attaches the context", function (t) { 87 | t.plan(3); 88 | 89 | var namespace = cls.createNamespace('cls@nexttick'); 90 | var d = domain.create(); 91 | namespace.bindEmitter(d); 92 | 93 | d.on('error', function (e) { 94 | t.ok(namespace.fromException(e), "context was attached to error"); 95 | t.equal(namespace.fromException(e)['value'], 'transaction set', 96 | "found the inner value"); 97 | 98 | cls.destroyNamespace('cls@nexttick'); 99 | }); 100 | 101 | namespace.run(function () { 102 | namespace.set('value', 'transaction clear'); 103 | 104 | // tap is only trying to help 105 | process.nextTick(d.bind(function () { 106 | namespace.run(function () { 107 | namespace.set('value', 'transaction set'); 108 | throw new Error("cls@nexttick explosion"); 109 | }); 110 | })); 111 | 112 | t.equal(namespace.get('value'), 'transaction clear', "everything was reset"); 113 | }); 114 | }); 115 | 116 | test("throw in setTimeout attaches the context", function (t) { 117 | t.plan(3); 118 | 119 | var namespace = cls.createNamespace('cls@setTimeout'); 120 | var d = domain.create(); 121 | namespace.bindEmitter(d); 122 | 123 | d.on('error', function (e) { 124 | t.ok(namespace.fromException(e), "context was attached to error"); 125 | t.equal(namespace.fromException(e)['value'], 'transaction set', 126 | "found the inner value"); 127 | 128 | cls.destroyNamespace('cls@setTimeout'); 129 | }); 130 | 131 | namespace.run(function () { 132 | namespace.set('value', 'transaction clear'); 133 | 134 | // tap is only trying to help 135 | setTimeout(d.bind(function () { 136 | namespace.run(function () { 137 | namespace.set('value', 'transaction set'); 138 | throw new Error("cls@setTimeout explosion"); 139 | }); 140 | })); 141 | 142 | t.equal(namespace.get('value'), 'transaction clear', "everything was reset"); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v3.1.0 (2014-07-28): 2 | 3 | * Updated to use `async-listener@0.4.7` to pick up bug fixes. 4 | 5 | ### v3.0.0 (2013-12-14): 6 | 7 | * Removed the notion of a "default" or "global" context per namespace. It only 8 | existed to create a simpler interface for developing and testing the module, 9 | and created the potential for nasty information disclosure bugs (see [issue 10 | #14](https://github.com/othiym23/node-continuation-local-storage/issues/14) 11 | for details). This is potentially a breaking change, if you're depending on 12 | the global context, so semver says we have to bump the major version. 13 | * Added this changelog. 14 | 15 | ### v2.6.2 (2013-12-07): 16 | 17 | * `async-listener` and `emitter-listener` dependency refresh. 18 | 19 | ### v2.6.1 (2013-11-29): 20 | 21 | * `emitter-listener` has been extracted from `shimmer` into a standalone 22 | module for `namespace.bindEmitter()`. 23 | 24 | ### v2.6.0 (2013-11-27): 25 | 26 | * When an error is thrown in a CLS-bound continuation chain, attach the active 27 | context for the namespace to which the chain is bound. This is necessary 28 | because CLS and asyncListeners actually do too good a job of cleaning up 29 | after errors, and so they don't escape the continuation chain. New Relic 30 | needs the context so it can get the transaction active when the error 31 | happened for error tracing. 32 | 33 | ### v2.5.2 (2013-10-30): 34 | 35 | * `async-listener` dependency refresh for better support of node 0.8.0 - 0.8.3. 36 | 37 | ### v2.5.1 (2013-10-27): 38 | 39 | * `async-listener` dependency refresh. 40 | 41 | ### v2.5.0 (2013-10-27): 42 | 43 | * Relax the requirement that CLS contexts be pushed and popped from a stack, 44 | instead treating them as a set. This allows context interleaving (i.e. 45 | using the lower-level `namespace.enter()` and `namespace.exit()` API without 46 | any strict ordering dependencies). Everything works, but this still makes me 47 | a little uneasy. 48 | * EEs can now be bound to multiple namespaces, although this is likely to be 49 | slow. 50 | 51 | ### v2.4.4 (2013-10-27): 52 | 53 | * Even if you use an EE bound to a namespace outside a continuation chain, it 54 | shouldn't explode. 55 | 56 | ### v2.4.3 (2013-10-16): 57 | 58 | * `async-listener` dependency refresh. 59 | 60 | ### v2.4.2 (2013-10-13): 61 | 62 | * More tweaks for `async-listener` error handlers (just a dependency refresh). 63 | 64 | ### v2.4.1 (2013-10-12): 65 | 66 | * `async-listener` error listeners have gotten lots of tweaks. Update to newest 67 | API. 68 | * Only exit namespace context on error if a continuation chain is active. 69 | 70 | ### v2.4.0 (2013-10-11): 71 | 72 | * `async-listener` now supports error listeners. Update to newest API. 73 | * Namespace context should be exited on asynchronous errors. 74 | 75 | ### v2.3.4 (2013-10-03): 76 | 77 | * When EEs are in the middle of emitting, make sure that calls to 78 | `emitter.removeListener` are testing against non-monkeypatched versions of 79 | the event handlers (necessary so certain Connect middleware functions, such 80 | as `connect.limit`, run correctly). 81 | 82 | ### v2.3.3 (2013-10-02): 83 | 84 | * Ensure handler rebinding gets called even in case of errors. 85 | * Be consistent about making sure contexts are kept in a sane state when errors 86 | are thrown in EEs. 87 | 88 | ### v2.3.2 (2013-10-02): 89 | 90 | * Guard `on` / `addListener` remonkeypatching in `namespace.bindEmitter()` so 91 | that `shimmer` is only called to rebind if the monkeypatched versions have 92 | actually been replaced. 93 | * Don't try to call emit if there are no listeners on a bound EE. 94 | * Don't use `setImmediate` in tests, because it's not available in Node 0.8.x. 95 | 96 | ### v2.3.1 (2013-10-01): 97 | 98 | * Update to newest version of `async-listener`. 99 | * Fix typo. 100 | 101 | ### v2.3.0 (2013-09-30): 102 | 103 | * EventEmitters can now be bound to CLS namespaces. Because EEs act as coupling 104 | points between asynchronous domains, it's necessary for the EE binding to 105 | capture the CLS context both when the listener is added, and when a matching 106 | handler is firing because of a matching event being emitted. 107 | 108 | ### v2.2.1 (2013-09-30): 109 | 110 | * More tweaks to conform with `asyncListener` API changes. 111 | * Many more test cases to ensure `asyncListener` stuff is working with Node 112 | 0.8.x. 113 | 114 | ### v2.2.0 (2013-09-26): 115 | 116 | * Square up with latest `async-listener` / node PR #6011 changes. 117 | 118 | ### v2.1.2 (2013-09-09): 119 | 120 | * Document `namespace.createContext()`. 121 | * Fix issue where a value was *always* being returned from `namespace.run()`, 122 | even on error. 123 | 124 | ### v2.1.1 (2013-09-03): 125 | 126 | * Clean up minor typo in docs. 127 | 128 | ### v2.1.0 (2013-09-03): 129 | 130 | * Incorporate documentation from failed CLS PR. 131 | * `namespace.bind()` now also always exits the domain, even on error. 132 | * Namespaces can be destroyed. 133 | * `cls.reset()` allows tests to nuke all existing namespaces (use with care 134 | obviously). 135 | 136 | ### v2.0.0 (2013-09-01): 137 | 138 | * Use `async-listener` polyfill instead of `cls-glue`. 139 | * Incorporate tests from `cls-glue`. 140 | 141 | ### v1.1.1 (2013-09-01): 142 | 143 | * Namespace exits context even on error. 144 | 145 | ### v1.1.0 (2013-07-30): 146 | 147 | * Split createContext so it's part of the namespace API. 148 | * Tweak error message to be more informative. 149 | 150 | ### v1.0.1 (2013-07-25): 151 | 152 | * Correct Tim's email address. 153 | 154 | ### v1.0.0 (2013-07-25): 155 | 156 | * Each application of CLS is allocated its own "namespace", which bind data to 157 | continuation chains, either using `.run()` or `.bind()` to create a new 158 | nested context. These nested contexts are prototype chains that point back to 159 | a "default" / "global" context, with the default context for each namespace 160 | being a prototype-free "data bag" created with `Object.create(null)`. 161 | 162 | ### v0.1.1 (2013-05-03): 163 | 164 | * Document progress thus far. 165 | 166 | ### v0.1.0 (2013-05-03): 167 | 168 | * First attempt: basic API, docs, and tests. 169 | -------------------------------------------------------------------------------- /context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var wrapEmitter = require('emitter-listener'); 5 | 6 | /* 7 | * 8 | * CONSTANTS 9 | * 10 | */ 11 | var CONTEXTS_SYMBOL = 'cls@contexts'; 12 | var ERROR_SYMBOL = 'error@context'; 13 | 14 | // load polyfill if native support is unavailable 15 | if (!process.addAsyncListener) require('async-listener'); 16 | 17 | function Namespace(name) { 18 | this.name = name; 19 | // changed in 2.7: no default context 20 | this.active = null; 21 | this._set = []; 22 | this.id = null; 23 | } 24 | 25 | Namespace.prototype.set = function (key, value) { 26 | if (!this.active) { 27 | throw new Error("No context available. ns.run() or ns.bind() must be called first."); 28 | } 29 | 30 | this.active[key] = value; 31 | return value; 32 | }; 33 | 34 | Namespace.prototype.get = function (key) { 35 | if (!this.active) return undefined; 36 | 37 | return this.active[key]; 38 | }; 39 | 40 | Namespace.prototype.createContext = function () { 41 | return Object.create(this.active); 42 | }; 43 | 44 | Namespace.prototype.run = function (fn) { 45 | var context = this.createContext(); 46 | this.enter(context); 47 | try { 48 | fn(context); 49 | return context; 50 | } 51 | catch (exception) { 52 | if (exception) { 53 | exception[ERROR_SYMBOL] = context; 54 | } 55 | throw exception; 56 | } 57 | finally { 58 | this.exit(context); 59 | } 60 | }; 61 | 62 | Namespace.prototype.runAndReturn = function (fn) { 63 | var value; 64 | this.run(function (context) { 65 | value = fn(context); 66 | }); 67 | return value; 68 | }; 69 | 70 | Namespace.prototype.bind = function (fn, context) { 71 | if (!context) { 72 | if (!this.active) { 73 | context = this.createContext(); 74 | } 75 | else { 76 | context = this.active; 77 | } 78 | } 79 | 80 | var self = this; 81 | return function () { 82 | self.enter(context); 83 | try { 84 | return fn.apply(this, arguments); 85 | } 86 | catch (exception) { 87 | if (exception) { 88 | exception[ERROR_SYMBOL] = context; 89 | } 90 | throw exception; 91 | } 92 | finally { 93 | self.exit(context); 94 | } 95 | }; 96 | }; 97 | 98 | Namespace.prototype.enter = function (context) { 99 | assert.ok(context, "context must be provided for entering"); 100 | 101 | this._set.push(this.active); 102 | this.active = context; 103 | }; 104 | 105 | Namespace.prototype.exit = function (context) { 106 | assert.ok(context, "context must be provided for exiting"); 107 | 108 | // Fast path for most exits that are at the top of the stack 109 | if (this.active === context) { 110 | assert.ok(this._set.length, "can't remove top context"); 111 | this.active = this._set.pop(); 112 | return; 113 | } 114 | 115 | // Fast search in the stack using lastIndexOf 116 | var index = this._set.lastIndexOf(context); 117 | 118 | assert.ok(index >= 0, "context not currently entered; can't exit"); 119 | assert.ok(index, "can't remove top context"); 120 | 121 | this._set.splice(index, 1); 122 | }; 123 | 124 | Namespace.prototype.bindEmitter = function (emitter) { 125 | assert.ok(emitter.on && emitter.addListener && emitter.emit, "can only bind real EEs"); 126 | 127 | var namespace = this; 128 | var thisSymbol = 'context@' + this.name; 129 | 130 | // Capture the context active at the time the emitter is bound. 131 | function attach(listener) { 132 | if (!listener) return; 133 | if (!listener[CONTEXTS_SYMBOL]) listener[CONTEXTS_SYMBOL] = Object.create(null); 134 | 135 | listener[CONTEXTS_SYMBOL][thisSymbol] = { 136 | namespace : namespace, 137 | context : namespace.active 138 | }; 139 | } 140 | 141 | // At emit time, bind the listener within the correct context. 142 | function bind(unwrapped) { 143 | if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) return unwrapped; 144 | 145 | var wrapped = unwrapped; 146 | var contexts = unwrapped[CONTEXTS_SYMBOL]; 147 | Object.keys(contexts).forEach(function (name) { 148 | var thunk = contexts[name]; 149 | wrapped = thunk.namespace.bind(wrapped, thunk.context); 150 | }); 151 | return wrapped; 152 | } 153 | 154 | wrapEmitter(emitter, attach, bind); 155 | }; 156 | 157 | /** 158 | * If an error comes out of a namespace, it will have a context attached to it. 159 | * This function knows how to find it. 160 | * 161 | * @param {Error} exception Possibly annotated error. 162 | */ 163 | Namespace.prototype.fromException = function (exception) { 164 | return exception[ERROR_SYMBOL]; 165 | }; 166 | 167 | function get(name) { 168 | return process.namespaces[name]; 169 | } 170 | 171 | function create(name) { 172 | assert.ok(name, "namespace must be given a name!"); 173 | 174 | var namespace = new Namespace(name); 175 | namespace.id = process.addAsyncListener({ 176 | create : function () { return namespace.active; }, 177 | before : function (context, storage) { if (storage) namespace.enter(storage); }, 178 | after : function (context, storage) { if (storage) namespace.exit(storage); }, 179 | error : function (storage) { if (storage) namespace.exit(storage); } 180 | }); 181 | 182 | process.namespaces[name] = namespace; 183 | return namespace; 184 | } 185 | 186 | function destroy(name) { 187 | var namespace = get(name); 188 | 189 | assert.ok(namespace, "can't delete nonexistent namespace!"); 190 | assert.ok(namespace.id, "don't assign to process.namespaces directly!"); 191 | 192 | process.removeAsyncListener(namespace.id); 193 | process.namespaces[name] = null; 194 | } 195 | 196 | function reset() { 197 | // must unregister async listeners 198 | if (process.namespaces) { 199 | Object.keys(process.namespaces).forEach(function (name) { 200 | destroy(name); 201 | }); 202 | } 203 | process.namespaces = Object.create(null); 204 | } 205 | if (!process.namespaces) reset(); // call immediately to set up 206 | 207 | module.exports = { 208 | getNamespace : get, 209 | createNamespace : create, 210 | destroyNamespace : destroy, 211 | reset : reset 212 | }; 213 | -------------------------------------------------------------------------------- /test/dns.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dns = require('dns') 4 | , tap = require('tap') 5 | , test = tap.test 6 | , createNamespace = require('../context.js').createNamespace 7 | ; 8 | 9 | test("continuation-local state with MakeCallback and DNS module", function (t) { 10 | t.plan(11); 11 | 12 | var namespace = createNamespace('dns'); 13 | namespace.run(function () { 14 | namespace.set('test', 0xabad1dea); 15 | 16 | t.test("dns.lookup", function (t) { 17 | namespace.run(function () { 18 | namespace.set('test', 808); 19 | t.equal(namespace.get('test'), 808, "state has been mutated"); 20 | 21 | dns.lookup('www.newrelic.com', 4, function (err, addresses) { 22 | t.notOk(err, "lookup succeeded"); 23 | t.ok(addresses.length > 0, "some results were found"); 24 | 25 | t.equal(namespace.get('test'), 808, 26 | "mutated state has persisted to dns.lookup's callback"); 27 | 28 | t.end(); 29 | }); 30 | }); 31 | }); 32 | 33 | t.test("dns.resolve", function (t) { 34 | namespace.run(function () { 35 | namespace.set('test', 909); 36 | t.equal(namespace.get('test'), 909, "state has been mutated"); 37 | 38 | dns.resolve('newrelic.com', 'NS', function (err, addresses) { 39 | t.notOk(err, "lookup succeeded"); 40 | t.ok(addresses.length > 0, "some results were found"); 41 | 42 | t.equal(namespace.get('test'), 909, 43 | "mutated state has persisted to dns.resolve's callback"); 44 | 45 | t.end(); 46 | }); 47 | }); 48 | }); 49 | 50 | t.test("dns.resolve4", function (t) { 51 | namespace.run(function () { 52 | namespace.set('test', 303); 53 | t.equal(namespace.get('test'), 303, "state has been mutated"); 54 | 55 | dns.resolve4('www.newrelic.com', function (err, addresses) { 56 | t.notOk(err, "lookup succeeded"); 57 | t.ok(addresses.length > 0, "some results were found"); 58 | 59 | t.equal(namespace.get('test'), 303, 60 | "mutated state has persisted to dns.resolve4's callback"); 61 | 62 | t.end(); 63 | }); 64 | }); 65 | }); 66 | 67 | t.test("dns.resolve6", function (t) { 68 | namespace.run(function () { 69 | namespace.set('test', 101); 70 | t.equal(namespace.get('test'), 101, "state has been mutated"); 71 | 72 | dns.resolve6('google.com', function (err, addresses) { 73 | t.notOk(err, "lookup succeeded"); 74 | t.ok(addresses.length > 0, "some results were found"); 75 | 76 | t.equal(namespace.get('test'), 101, 77 | "mutated state has persisted to dns.resolve6's callback"); 78 | 79 | t.end(); 80 | }); 81 | }); 82 | }); 83 | 84 | t.test("dns.resolveCname", function (t) { 85 | namespace.run(function () { 86 | namespace.set('test', 212); 87 | t.equal(namespace.get('test'), 212, "state has been mutated"); 88 | 89 | dns.resolveCname('mail.newrelic.com', function (err, addresses) { 90 | t.notOk(err, "lookup succeeded"); 91 | t.ok(addresses.length > 0, "some results were found"); 92 | 93 | t.equal(namespace.get('test'), 212, 94 | "mutated state has persisted to dns.resolveCname's callback"); 95 | 96 | t.end(); 97 | }); 98 | }); 99 | }); 100 | 101 | t.test("dns.resolveMx", function (t) { 102 | namespace.run(function () { 103 | namespace.set('test', 707); 104 | t.equal(namespace.get('test'), 707, "state has been mutated"); 105 | 106 | dns.resolveMx('newrelic.com', function (err, addresses) { 107 | t.notOk(err, "lookup succeeded"); 108 | t.ok(addresses.length > 0, "some results were found"); 109 | 110 | t.equal(namespace.get('test'), 707, 111 | "mutated state has persisted to dns.resolveMx's callback"); 112 | 113 | t.end(); 114 | }); 115 | }); 116 | }); 117 | 118 | t.test("dns.resolveNs", function (t) { 119 | namespace.run(function () { 120 | namespace.set('test', 717); 121 | t.equal(namespace.get('test'), 717, "state has been mutated"); 122 | 123 | dns.resolveNs('newrelic.com', function (err, addresses) { 124 | t.notOk(err, "lookup succeeded"); 125 | t.ok(addresses.length > 0, "some results were found"); 126 | 127 | t.equal(namespace.get('test'), 717, 128 | "mutated state has persisted to dns.resolveNs's callback"); 129 | 130 | t.end(); 131 | }); 132 | }); 133 | }); 134 | 135 | t.test("dns.resolveTxt", function (t) { 136 | namespace.run(function () { 137 | namespace.set('test', 2020); 138 | t.equal(namespace.get('test'), 2020, "state has been mutated"); 139 | 140 | dns.resolveTxt('newrelic.com', function (err, addresses) { 141 | t.notOk(err, "lookup succeeded"); 142 | t.ok(addresses.length > 0, "some results were found"); 143 | 144 | t.equal(namespace.get('test'), 2020, 145 | "mutated state has persisted to dns.resolveTxt's callback"); 146 | 147 | t.end(); 148 | }); 149 | }); 150 | }); 151 | 152 | t.test("dns.resolveSrv", function (t) { 153 | namespace.run(function () { 154 | namespace.set('test', 9000); 155 | t.equal(namespace.get('test'), 9000, "state has been mutated"); 156 | 157 | dns.resolveSrv('_xmpp-server._tcp.google.com', function (err, addresses) { 158 | t.notOk(err, "lookup succeeded"); 159 | t.ok(addresses.length > 0, "some results were found"); 160 | 161 | t.equal(namespace.get('test'), 9000, 162 | "mutated state has persisted to dns.resolveSrv's callback"); 163 | 164 | t.end(); 165 | }); 166 | }); 167 | }); 168 | 169 | t.test("dns.resolveNaptr", function (t) { 170 | // dns.resolveNaptr only in Node > 0.9.x 171 | if (!dns.resolveNaptr) return t.end(); 172 | 173 | namespace.run(function () { 174 | namespace.set('test', 'Polysix'); 175 | t.equal(namespace.get('test'), 'Polysix', "state has been mutated"); 176 | 177 | dns.resolveNaptr('columbia.edu', function (err, addresses) { 178 | t.notOk(err, "lookup succeeded"); 179 | t.ok(addresses.length > 0, "some results were found"); 180 | 181 | t.equal(namespace.get('test'), 'Polysix', 182 | "mutated state has persisted to dns.resolveNaptr's callback"); 183 | 184 | t.end(); 185 | }); 186 | }); 187 | }); 188 | 189 | t.test("dns.reverse", function (t) { 190 | namespace.run(function () { 191 | namespace.set('test', 1000); 192 | t.equal(namespace.get('test'), 1000, "state has been mutated"); 193 | 194 | dns.reverse('204.93.223.144', function (err, addresses) { 195 | t.notOk(err, "lookup succeeded"); 196 | t.ok(addresses.length > 0, "some results were found"); 197 | 198 | t.equal(namespace.get('test'), 1000, 199 | "mutated state has persisted to dns.reverse's callback"); 200 | 201 | t.end(); 202 | }); 203 | }); 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/continuation-local-storage.png?downloads=true&stars=true)](https://nodei.co/npm/continuation-local-storage/) 2 | 3 | # Continuation-Local Storage 4 | 5 | Continuation-local storage works like thread-local storage in threaded 6 | programming, but is based on chains of Node-style callbacks instead of threads. 7 | The standard Node convention of functions calling functions is very similar to 8 | something called ["continuation-passing style"][cps] in functional programming, 9 | and the name comes from the way this module allows you to set and get values 10 | that are scoped to the lifetime of these chains of function calls. 11 | 12 | Suppose you're writing a module that fetches a user and adds it to a session 13 | before calling a function passed in by a user to continue execution: 14 | 15 | ```javascript 16 | // setup.js 17 | 18 | var createNamespace = require('continuation-local-storage').createNamespace; 19 | var session = createNamespace('my session'); 20 | 21 | var db = require('./lib/db.js'); 22 | 23 | function start(options, next) { 24 | db.fetchUserById(options.id, function (error, user) { 25 | if (error) return next(error); 26 | 27 | session.set('user', user); 28 | 29 | next(); 30 | }); 31 | } 32 | ``` 33 | 34 | Later on in the process of turning that user's data into an HTML page, you call 35 | another function (maybe defined in another module entirely) that wants to fetch 36 | the value you set earlier: 37 | 38 | ```javascript 39 | // send_response.js 40 | 41 | var getNamespace = require('continuation-local-storage').getNamespace; 42 | var session = getNamespace('my session'); 43 | 44 | var render = require('./lib/render.js') 45 | 46 | function finish(response) { 47 | var user = session.get('user'); 48 | render({user: user}).pipe(response); 49 | } 50 | ``` 51 | 52 | When you set values in continuation-local storage, those values are accessible 53 | until all functions called from the original function – synchronously or 54 | asynchronously – have finished executing. This includes callbacks passed to 55 | `process.nextTick` and the [timer functions][] ([setImmediate][], 56 | [setTimeout][], and [setInterval][]), as well as callbacks passed to 57 | asynchronous functions that call native functions (such as those exported from 58 | the `fs`, `dns`, `zlib` and `crypto` modules). 59 | 60 | A simple rule of thumb is anywhere where you might have set a property on the 61 | `request` or `response` objects in an HTTP handler, you can (and should) now 62 | use continuation-local storage. This API is designed to allow you extend the 63 | scope of a variable across a sequence of function calls, but with values 64 | specific to each sequence of calls. 65 | 66 | Values are grouped into namespaces, created with `createNamespace()`. Sets of 67 | function calls are grouped together by calling them within the function passed 68 | to `.run()` on the namespace object. Calls to `.run()` can be nested, and each 69 | nested context this creates has its own copy of the set of values from the 70 | parent context. When a function is making multiple asynchronous calls, this 71 | allows each child call to get, set, and pass along its own context without 72 | overwriting the parent's. 73 | 74 | A simple, annotated example of how this nesting behaves: 75 | 76 | ```javascript 77 | var createNamespace = require('continuation-local-storage').createNamespace; 78 | 79 | var writer = createNamespace('writer'); 80 | writer.run(function () { 81 | writer.set('value', 0); 82 | 83 | requestHandler(); 84 | }); 85 | 86 | function requestHandler() { 87 | writer.run(function(outer) { 88 | // writer.get('value') returns 0 89 | // outer.value is 0 90 | writer.set('value', 1); 91 | // writer.get('value') returns 1 92 | // outer.value is 1 93 | process.nextTick(function() { 94 | // writer.get('value') returns 1 95 | // outer.value is 1 96 | writer.run(function(inner) { 97 | // writer.get('value') returns 1 98 | // outer.value is 1 99 | // inner.value is 1 100 | writer.set('value', 2); 101 | // writer.get('value') returns 2 102 | // outer.value is 1 103 | // inner.value is 2 104 | }); 105 | }); 106 | }); 107 | 108 | setTimeout(function() { 109 | // runs with the default context, because nested contexts have ended 110 | console.log(writer.get('value')); // prints 0 111 | }, 1000); 112 | } 113 | ``` 114 | 115 | ## cls.createNamespace(name) 116 | 117 | * return: {Namespace} 118 | 119 | Each application wanting to use continuation-local values should create its own 120 | namespace. Reading from (or, more significantly, writing to) namespaces that 121 | don't belong to you is a faux pas. 122 | 123 | ## cls.getNamespace(name) 124 | 125 | * return: {Namespace} 126 | 127 | Look up an existing namespace. 128 | 129 | ## cls.destroyNamespace(name) 130 | 131 | Dispose of an existing namespace. WARNING: be sure to dispose of any references 132 | to destroyed namespaces in your old code, as contexts associated with them will 133 | no longer be propagated. 134 | 135 | ## cls.reset() 136 | 137 | Completely reset all continuation-local storage namespaces. WARNING: while this 138 | will stop the propagation of values in any existing namespaces, if there are 139 | remaining references to those namespaces in code, the associated storage will 140 | still be reachable, even though the associated state is no longer being updated. 141 | Make sure you clean up any references to destroyed namespaces yourself. 142 | 143 | ## process.namespaces 144 | 145 | * return: dictionary of {Namespace} objects 146 | 147 | Continuation-local storage has a performance cost, and so it isn't enabled 148 | until the module is loaded for the first time. Once the module is loaded, the 149 | current set of namespaces is available in `process.namespaces`, so library code 150 | that wants to use continuation-local storage only when it's active should test 151 | for the existence of `process.namespaces`. 152 | 153 | ## Class: Namespace 154 | 155 | Application-specific namespaces group values local to the set of functions 156 | whose calls originate from a callback passed to `namespace.run()` or 157 | `namespace.bind()`. 158 | 159 | ### namespace.active 160 | 161 | * return: the currently active context on a namespace 162 | 163 | ### namespace.set(key, value) 164 | 165 | * return: `value` 166 | 167 | Set a value on the current continuation context. Must be set within an active 168 | continuation chain started with `namespace.run()` or `namespace.bind()`. 169 | 170 | ### namespace.get(key) 171 | 172 | * return: the requested value, or `undefined` 173 | 174 | Look up a value on the current continuation context. Recursively searches from 175 | the innermost to outermost nested continuation context for a value associated 176 | with a given key. Must be set within an active continuation chain started with 177 | `namespace.run()` or `namespace.bind()`. 178 | 179 | ### namespace.run(callback) 180 | 181 | * return: the context associated with that callback 182 | 183 | Create a new context on which values can be set or read. Run all the functions 184 | that are called (either directly, or indirectly through asynchronous functions 185 | that take callbacks themselves) from the provided callback within the scope of 186 | that namespace. The new context is passed as an argument to the callback 187 | when it's called. 188 | 189 | ### namespace.runAndReturn(callback) 190 | 191 | * return: the return value of the callback 192 | 193 | Create a new context on which values can be set or read. Run all the functions 194 | that are called (either directly, or indirectly through asynchronous functions 195 | that take callbacks themselves) from the provided callback within the scope of 196 | that namespace. The new context is passed as an argument to the callback 197 | when it's called. 198 | 199 | Same as `namespace.run()` but returns the return value of the callback rather 200 | than the context. 201 | 202 | ### namespace.bind(callback, [context]) 203 | 204 | * return: a callback wrapped up in a context closure 205 | 206 | Bind a function to the specified namespace. Works analogously to 207 | `Function.bind()` or `domain.bind()`. If context is omitted, it will default to 208 | the currently active context in the namespace, or create a new context if none 209 | is currently defined. 210 | 211 | ### namespace.bindEmitter(emitter) 212 | 213 | Bind an EventEmitter to a namespace. Operates similarly to `domain.add`, with a 214 | less generic name and the additional caveat that unlike domains, namespaces 215 | never implicitly bind EventEmitters to themselves when they're created within 216 | the context of an active namespace. 217 | 218 | The most likely time you'd want to use this is when you're using Express or 219 | Connect and want to make sure your middleware execution plays nice with CLS, or 220 | are doing other things with HTTP listeners: 221 | 222 | ```javascript 223 | http.createServer(function (req, res) { 224 | writer.bindEmitter(req); 225 | writer.bindEmitter(res); 226 | 227 | // do other stuff, some of which is asynchronous 228 | }); 229 | ``` 230 | 231 | ### namespace.createContext() 232 | 233 | * return: a context cloned from the currently active context 234 | 235 | Use this with `namespace.bind()`, if you want to have a fresh context at invocation time, 236 | as opposed to binding time: 237 | 238 | ```javascript 239 | function doSomething(p) { 240 | console.log("%s = %s", p, ns.get(p)); 241 | } 242 | 243 | function bindLater(callback) { 244 | return writer.bind(callback, writer.createContext()); 245 | } 246 | 247 | setInterval(function () { 248 | var bound = bindLater(doSomething); 249 | bound('test'); 250 | }, 100); 251 | ``` 252 | 253 | ## context 254 | 255 | A context is a plain object created using the enclosing context as its prototype. 256 | 257 | # copyright & license 258 | 259 | See [LICENSE](https://github.com/othiym23/node-continuation-local-storage/blob/master/LICENSE) 260 | for the details of the BSD 2-clause "simplified" license used by 261 | `continuation-local-storage`. This package was developed in 2012-2013 (and is 262 | maintained now) by Forrest L Norvell, [@othiym23](https://github.com/othiym23), 263 | with considerable help from Timothy Caswell, 264 | [@creationix](https://github.com/creationix), working for The Node Firm. This 265 | work was underwritten by New Relic for use in their Node.js instrumentation 266 | agent, so maybe give that a look if you have some Node.js 267 | performance-monitoring needs. 268 | 269 | [timer functions]: https://nodejs.org/api/timers.html 270 | [setImmediate]: https://nodejs.org/api/timers.html#timers_setimmediate_callback_arg 271 | [setTimeout]: https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg 272 | [setInterval]: https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_arg 273 | [cps]: http://en.wikipedia.org/wiki/Continuation-passing_style 274 | -------------------------------------------------------------------------------- /test/tracer-scenarios.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter 4 | , assert = require('assert') 5 | , test = require('tap').test 6 | , cls = require('../context.js') 7 | ; 8 | 9 | var nextID = 1; 10 | function fresh(name) { 11 | assert.ok(!cls.getNamespace(name), "namespace " + name + " already exists"); 12 | return cls.createNamespace(name); 13 | } 14 | 15 | function destroy(name) { 16 | return function destroyer(t) { 17 | cls.destroyNamespace(name); 18 | assert.ok(!cls.getNamespace(name), "namespace '" + name + "' should no longer exist"); 19 | t.end(); 20 | }; 21 | } 22 | 23 | function runInTransaction(name, fn) { 24 | var namespace = cls.getNamespace(name); 25 | assert(namespace, "namespaces " + name + " doesn't exist"); 26 | 27 | var context = namespace.createContext(); 28 | context.transaction = ++nextID; 29 | process.nextTick(namespace.bind(fn, context)); 30 | } 31 | 32 | test("asynchronous state propagation", function (t) { 33 | t.plan(24); 34 | 35 | t.test("a. async transaction with setTimeout", function (t) { 36 | t.plan(2); 37 | 38 | var namespace = fresh('a', this); 39 | 40 | function handler() { 41 | t.ok(namespace.get('transaction'), "transaction should be visible"); 42 | } 43 | 44 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 45 | runInTransaction('a', function () { setTimeout(handler, 100); }); 46 | }); 47 | 48 | t.test("a. cleanup", destroy('a')); 49 | 50 | t.test("b. async transaction with setInterval", function (t) { 51 | t.plan(4); 52 | 53 | var namespace = fresh('b', this) 54 | , count = 0 55 | , handle 56 | ; 57 | 58 | function handler() { 59 | count += 1; 60 | if (count > 2) clearInterval(handle); 61 | t.ok(namespace.get('transaction'), "transaction should be visible"); 62 | } 63 | 64 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 65 | runInTransaction('b', function () { handle = setInterval(handler, 50); }); 66 | }); 67 | 68 | t.test("b. cleanup", destroy('b')); 69 | 70 | t.test("c. async transaction with process.nextTick", function (t) { 71 | t.plan(2); 72 | 73 | var namespace = fresh('c', this); 74 | 75 | function handler() { 76 | t.ok(namespace.get('transaction'), "transaction should be visible"); 77 | } 78 | 79 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 80 | runInTransaction('c', function () { process.nextTick(handler); }); 81 | }); 82 | 83 | t.test("c. cleanup", destroy('c')); 84 | 85 | t.test("d. async transaction with EventEmitter.emit", function (t) { 86 | t.plan(2); 87 | 88 | var namespace = fresh('d', this) 89 | , ee = new EventEmitter() 90 | ; 91 | 92 | function handler() { 93 | t.ok(namespace.get('transaction'), "transaction should be visible"); 94 | } 95 | 96 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 97 | runInTransaction('d', function () { 98 | ee.on('transaction', handler); 99 | ee.emit('transaction'); 100 | }); 101 | }); 102 | 103 | t.test("d. cleanup", destroy('d')); 104 | 105 | t.test("e. two overlapping async transactions with setTimeout", function (t) { 106 | t.plan(6); 107 | 108 | var namespace = fresh('e', this) 109 | , first 110 | , second 111 | ; 112 | 113 | function handler(id) { 114 | t.ok(namespace.get('transaction'), "transaction should be visible"); 115 | t.equal(namespace.get('transaction'), id, "transaction matches"); 116 | } 117 | 118 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 119 | runInTransaction('e', function () { 120 | first = namespace.get('transaction'); 121 | setTimeout(handler.bind(null, first), 100); 122 | }); 123 | 124 | setTimeout(function () { 125 | runInTransaction('e', function () { 126 | second = namespace.get('transaction'); 127 | t.notEqual(first, second, "different transaction IDs"); 128 | setTimeout(handler.bind(null, second), 100); 129 | }); 130 | }, 25); 131 | }); 132 | 133 | t.test("e. cleanup", destroy('e')); 134 | 135 | t.test("f. two overlapping async transactions with setInterval", function (t) { 136 | t.plan(15); 137 | 138 | var namespace = fresh('f', this); 139 | 140 | function runInterval() { 141 | var count = 0 142 | , handle 143 | , id 144 | ; 145 | 146 | function handler() { 147 | count += 1; 148 | if (count > 2) clearInterval(handle); 149 | t.ok(namespace.get('transaction'), "transaction should be visible"); 150 | t.equal(id, namespace.get('transaction'), "transaction ID should be immutable"); 151 | } 152 | 153 | function run() { 154 | t.ok(namespace.get('transaction'), "transaction should have been created"); 155 | id = namespace.get('transaction'); 156 | handle = setInterval(handler, 50); 157 | } 158 | 159 | runInTransaction('f', run); 160 | } 161 | 162 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 163 | runInterval(); runInterval(); 164 | }); 165 | 166 | t.test("f. cleanup", destroy('f')); 167 | 168 | t.test("g. two overlapping async transactions with process.nextTick", function (t) { 169 | t.plan(6); 170 | 171 | var namespace = fresh('g', this) 172 | , first 173 | , second 174 | ; 175 | 176 | function handler(id) { 177 | var transaction = namespace.get('transaction'); 178 | t.ok(transaction, "transaction should be visible"); 179 | t.equal(transaction, id, "transaction matches"); 180 | } 181 | 182 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 183 | runInTransaction('g', function () { 184 | first = namespace.get('transaction'); 185 | process.nextTick(handler.bind(null, first)); 186 | }); 187 | 188 | process.nextTick(function () { 189 | runInTransaction('g', function () { 190 | second = namespace.get('transaction'); 191 | t.notEqual(first, second, "different transaction IDs"); 192 | process.nextTick(handler.bind(null, second)); 193 | }); 194 | }); 195 | }); 196 | 197 | t.test("g. cleanup", destroy('g')); 198 | 199 | t.test("h. two overlapping async runs with EventEmitter.prototype.emit", function (t) { 200 | t.plan(3); 201 | 202 | var namespace = fresh('h', this) 203 | , ee = new EventEmitter() 204 | ; 205 | 206 | function handler() { 207 | t.ok(namespace.get('transaction'), "transaction should be visible"); 208 | } 209 | 210 | function lifecycle() { 211 | ee.once('transaction', process.nextTick.bind(process, handler)); 212 | ee.emit('transaction'); 213 | } 214 | 215 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 216 | runInTransaction('h', lifecycle); 217 | runInTransaction('h', lifecycle); 218 | }); 219 | 220 | t.test("h. cleanup", destroy('h')); 221 | 222 | t.test("i. async transaction with an async sub-call with setTimeout", function (t) { 223 | t.plan(5); 224 | 225 | var namespace = fresh('i', this); 226 | 227 | function inner(callback) { 228 | setTimeout(function () { 229 | t.ok(namespace.get('transaction'), "transaction should (yep) still be visible"); 230 | callback(); 231 | }, 50); 232 | } 233 | 234 | function outer() { 235 | t.ok(namespace.get('transaction'), "transaction should be visible"); 236 | setTimeout(function () { 237 | t.ok(namespace.get('transaction'), "transaction should still be visible"); 238 | inner(function () { 239 | t.ok(namespace.get('transaction'), "transaction should even still be visible"); 240 | }); 241 | }, 50); 242 | } 243 | 244 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 245 | runInTransaction('i', setTimeout.bind(null, outer, 50)); 246 | }); 247 | 248 | t.test("i. cleanup", destroy('i')); 249 | 250 | t.test("j. async transaction with an async sub-call with setInterval", function (t) { 251 | t.plan(5); 252 | 253 | var namespace = fresh('j', this) 254 | , outerHandle 255 | , innerHandle 256 | ; 257 | 258 | function inner(callback) { 259 | innerHandle = setInterval(function () { 260 | clearInterval(innerHandle); 261 | t.ok(namespace.get('transaction'), "transaction should (yep) still be visible"); 262 | callback(); 263 | }, 50); 264 | } 265 | 266 | function outer() { 267 | t.ok(namespace.get('transaction'), "transaction should be visible"); 268 | outerHandle = setInterval(function () { 269 | clearInterval(outerHandle); 270 | t.ok(namespace.get('transaction'), "transaction should still be visible"); 271 | inner(function () { 272 | t.ok(namespace.get('transaction'), "transaction should even still be visible"); 273 | }); 274 | }, 50); 275 | } 276 | 277 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 278 | runInTransaction('j', outer); 279 | }); 280 | 281 | t.test("j. cleanup", destroy('j')); 282 | 283 | t.test("k. async transaction with an async call with process.nextTick", function (t) { 284 | t.plan(5); 285 | 286 | var namespace = fresh('k', this); 287 | 288 | function inner(callback) { 289 | process.nextTick(function () { 290 | t.ok(namespace.get('transaction'), "transaction should (yep) still be visible"); 291 | callback(); 292 | }); 293 | } 294 | 295 | function outer() { 296 | t.ok(namespace.get('transaction'), "transaction should be visible"); 297 | process.nextTick(function () { 298 | t.ok(namespace.get('transaction'), "transaction should still be visible"); 299 | inner(function () { 300 | t.ok(namespace.get('transaction'), "transaction should even still be visible"); 301 | }); 302 | }); 303 | } 304 | 305 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 306 | runInTransaction('k', function () { process.nextTick(outer); }); 307 | }); 308 | 309 | t.test("k. cleanup", destroy('k')); 310 | 311 | t.test("l. async transaction with an async call with EventEmitter.emit", function (t) { 312 | t.plan(4); 313 | 314 | var namespace = fresh('l', this) 315 | , outer = new EventEmitter() 316 | , inner = new EventEmitter() 317 | ; 318 | 319 | inner.on('pong', function (callback) { 320 | t.ok(namespace.get('transaction'), "transaction should still be visible"); 321 | callback(); 322 | }); 323 | 324 | function outerCallback() { 325 | t.ok(namespace.get('transaction'), "transaction should even still be visible"); 326 | } 327 | 328 | outer.on('ping', function () { 329 | t.ok(namespace.get('transaction'), "transaction should be visible"); 330 | inner.emit('pong', outerCallback); 331 | }); 332 | 333 | t.notOk(namespace.get('transaction'), "transaction should not yet be visible"); 334 | runInTransaction('l', outer.emit.bind(outer, 'ping')); 335 | }); 336 | 337 | t.test("l. cleanup", destroy('l')); 338 | }); 339 | -------------------------------------------------------------------------------- /test/bind-emitter.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tap').test 4 | , EventEmitter = require('events').EventEmitter 5 | , cls = require('../context.js') 6 | ; 7 | 8 | test("event emitters bound to CLS context", function (t) { 9 | t.plan(13); 10 | 11 | t.test("handler registered in context, emit out of context", function (t) { 12 | t.plan(1); 13 | 14 | var n = cls.createNamespace('in') 15 | , ee = new EventEmitter() 16 | ; 17 | 18 | n.run(function () { 19 | n.set('value', 'hello'); 20 | n.bindEmitter(ee); 21 | ee.on('event', function () { 22 | t.equal(n.get('value'), 'hello', "value still set in EE."); 23 | cls.destroyNamespace('in'); 24 | }); 25 | }); 26 | 27 | ee.emit('event'); 28 | }); 29 | 30 | t.test("once handler registered in context", function (t) { 31 | t.plan(1); 32 | 33 | var n = cls.createNamespace('inOnce') 34 | , ee = new EventEmitter() 35 | ; 36 | 37 | n.run(function () { 38 | n.set('value', 'hello'); 39 | n.bindEmitter(ee); 40 | ee.once('event', function () { 41 | t.equal(n.get('value'), 'hello', "value still set in EE."); 42 | cls.destroyNamespace('inOnce'); 43 | }); 44 | }); 45 | 46 | ee.emit('event'); 47 | }); 48 | 49 | t.test("handler registered out of context, emit in context", function (t) { 50 | t.plan(1); 51 | 52 | var n = cls.createNamespace('out') 53 | , ee = new EventEmitter() 54 | ; 55 | 56 | ee.on('event', function () { 57 | t.equal(n.get('value'), 'hello', "value still set in EE."); 58 | cls.destroyNamespace('out'); 59 | }); 60 | 61 | n.run(function () { 62 | n.set('value', 'hello'); 63 | n.bindEmitter(ee); 64 | 65 | ee.emit('event'); 66 | }); 67 | }); 68 | 69 | t.test("once handler registered out of context", function (t) { 70 | t.plan(1); 71 | 72 | var n = cls.createNamespace('outOnce') 73 | , ee = new EventEmitter() 74 | ; 75 | 76 | ee.once('event', function () { 77 | t.equal(n.get('value'), 'hello', "value still set in EE."); 78 | cls.destroyNamespace('outOnce'); 79 | }); 80 | 81 | n.run(function () { 82 | n.set('value', 'hello'); 83 | n.bindEmitter(ee); 84 | 85 | ee.emit('event'); 86 | }); 87 | }); 88 | 89 | t.test("handler registered out of context, emit out of context", function (t) { 90 | t.plan(1); 91 | 92 | var n = cls.createNamespace('out') 93 | , ee = new EventEmitter() 94 | ; 95 | 96 | ee.on('event', function () { 97 | t.equal(n.get('value'), undefined, "no context."); 98 | cls.destroyNamespace('out'); 99 | }); 100 | 101 | n.run(function () { 102 | n.set('value', 'hello'); 103 | n.bindEmitter(ee); 104 | }); 105 | 106 | ee.emit('event'); 107 | }); 108 | 109 | t.test("once handler registered out of context on Readable", function (t) { 110 | var Readable = require('stream').Readable; 111 | 112 | if (Readable) { 113 | t.plan(12); 114 | 115 | var n = cls.createNamespace('outOnceReadable') 116 | , re = new Readable() 117 | ; 118 | 119 | re._read = function () {}; 120 | 121 | t.ok(n.name, "namespace has a name"); 122 | t.equal(n.name, 'outOnceReadable', "namespace has a name"); 123 | 124 | re.once('data', function (data) { 125 | t.equal(n.get('value'), 'hello', "value still set in EE"); 126 | t.equal(data, 'blah', "emit still works"); 127 | cls.destroyNamespace('outOnceReadable'); 128 | }); 129 | 130 | n.run(function () { 131 | n.set('value', 'hello'); 132 | 133 | t.notOk(re.emit.__wrapped, "emit is not wrapped"); 134 | t.notOk(re.on.__wrapped, "on is not wrapped"); 135 | t.notOk(re.addListener.__wrapped, "addListener is not wrapped"); 136 | 137 | n.bindEmitter(re); 138 | 139 | t.ok(re.emit.__wrapped, "emit is wrapped"); 140 | t.ok(re.on.__wrapped, "on is wrapped"); 141 | t.ok(re.addListener.__wrapped, "addListener is wrapped"); 142 | 143 | t.equal(typeof re._events.data, 'function', 'only the one data listener'); 144 | t.notOk(re._events.data['context@outOnceReadable'], "context isn't on listener"); 145 | 146 | re.emit('data', 'blah'); 147 | }); 148 | } 149 | else { 150 | t.comment("this test requires node 0.10+"); 151 | t.end(); 152 | } 153 | }); 154 | 155 | t.test("emitter with newListener that removes handler", function (t) { 156 | t.plan(3); 157 | 158 | var n = cls.createNamespace('newListener') 159 | , ee = new EventEmitter() 160 | ; 161 | 162 | // add monkeypatching to ee 163 | n.bindEmitter(ee); 164 | 165 | function listen() { 166 | ee.on('data', function (chunk) { 167 | t.equal(chunk, 'chunk', 'listener still works'); 168 | }); 169 | } 170 | 171 | ee.on('newListener', function handler(event) { 172 | if (event !== 'data') return; 173 | 174 | this.removeListener('newListener', handler); 175 | t.notOk(this.listeners('newListener').length, 'newListener was removed'); 176 | process.nextTick(listen); 177 | }); 178 | 179 | ee.on('drain', function (chunk) { 180 | process.nextTick(function () { 181 | ee.emit('data', chunk); 182 | }); 183 | }); 184 | 185 | ee.on('data', function (chunk) { 186 | t.equal(chunk, 'chunk', 'got data event'); 187 | cls.destroyNamespace('newListener'); 188 | }); 189 | 190 | ee.emit('drain', 'chunk'); 191 | }); 192 | 193 | t.test("handler registered in context on Readable", function (t) { 194 | var Readable = require('stream').Readable; 195 | 196 | if (Readable) { 197 | t.plan(12); 198 | 199 | var n = cls.createNamespace('outOnReadable') 200 | , re = new Readable() 201 | ; 202 | 203 | re._read = function () {}; 204 | 205 | t.ok(n.name, "namespace has a name"); 206 | t.equal(n.name, 'outOnReadable', "namespace has a name"); 207 | 208 | n.run(function () { 209 | n.set('value', 'hello'); 210 | 211 | n.bindEmitter(re); 212 | 213 | t.ok(re.emit.__wrapped, "emit is wrapped"); 214 | t.ok(re.on.__wrapped, "on is wrapped"); 215 | t.ok(re.addListener.__wrapped, "addListener is wrapped"); 216 | 217 | re.on('data', function (data) { 218 | t.equal(n.get('value'), 'hello', "value still set in EE"); 219 | t.equal(data, 'blah', "emit still works"); 220 | cls.destroyNamespace('outOnReadable'); 221 | }); 222 | }); 223 | 224 | t.ok(re.emit.__wrapped, "emit is still wrapped"); 225 | t.ok(re.on.__wrapped, "on is still wrapped"); 226 | t.ok(re.addListener.__wrapped, "addListener is still wrapped"); 227 | 228 | t.equal(typeof re._events.data, 'function', 'only the one data listener'); 229 | t.ok(re._events.data['cls@contexts']['context@outOnReadable'], 230 | "context is bound to listener"); 231 | 232 | re.emit('data', 'blah'); 233 | } 234 | else { 235 | t.comment("this test requires node 0.10+"); 236 | t.end(); 237 | } 238 | }); 239 | 240 | t.test("handler added but used entirely out of context", function (t) { 241 | t.plan(2); 242 | 243 | var n = cls.createNamespace('none') 244 | , ee = new EventEmitter() 245 | ; 246 | 247 | n.run(function () { 248 | n.set('value', 'hello'); 249 | n.bindEmitter(ee); 250 | }); 251 | 252 | ee.on('event', function () { 253 | t.ok(n, "n is set"); 254 | t.notOk(n.get('value'), "value shouldn't be visible"); 255 | cls.destroyNamespace('none'); 256 | }); 257 | 258 | ee.emit('event'); 259 | }); 260 | 261 | t.test("handler added but no listeners registered", function (t) { 262 | t.plan(3); 263 | 264 | var http = require('http') 265 | , n = cls.createNamespace('no_listener') 266 | ; 267 | 268 | // only fails on Node < 0.10 269 | var server = http.createServer(function (req, res) { 270 | n.bindEmitter(req); 271 | 272 | t.doesNotThrow(function () { 273 | req.emit('event'); 274 | }); 275 | 276 | res.writeHead(200, {"Content-Length" : 4}); 277 | res.end('WORD'); 278 | }); 279 | server.listen(8080); 280 | 281 | http.get('http://localhost:8080/', function (res) { 282 | t.equal(res.statusCode, 200, "request came back OK"); 283 | 284 | res.setEncoding('ascii'); 285 | res.on('data', function (body) { 286 | t.equal(body, 'WORD'); 287 | 288 | server.close(); 289 | cls.destroyNamespace('no_listener'); 290 | }); 291 | }); 292 | }); 293 | 294 | t.test("listener with parameters added but not bound to context", function (t) { 295 | t.plan(2); 296 | 297 | var ee = new EventEmitter() 298 | , n = cls.createNamespace('param_list') 299 | ; 300 | 301 | function sent(value) { 302 | t.equal(value, 3, "sent value is correct"); 303 | cls.destroyNamespace('param_list'); 304 | } 305 | 306 | ee.on('send', sent); 307 | n.bindEmitter(ee); 308 | t.doesNotThrow(function () { 309 | ee.emit('send', 3); 310 | }); 311 | }); 312 | 313 | t.test("listener that throws doesn't leave removeListener wrapped", function (t) { 314 | t.plan(4); 315 | 316 | var ee = new EventEmitter() 317 | , n = cls.createNamespace('kaboom') 318 | ; 319 | 320 | n.bindEmitter(ee); 321 | 322 | function kaboom() { 323 | throw new Error('whoops'); 324 | } 325 | 326 | n.run(function () { 327 | ee.on('bad', kaboom); 328 | 329 | t.throws(function () { ee.emit('bad'); }); 330 | t.equal(typeof ee.removeListener, 'function', 'removeListener is still there'); 331 | t.notOk(ee.removeListener.__wrapped, "removeListener got unwrapped"); 332 | t.equal(ee._events.bad, kaboom, "listener isn't still bound"); 333 | cls.destroyNamespace('kaboom'); 334 | }); 335 | }); 336 | 337 | t.test("emitter bound to multiple namespaces handles them correctly", function (t) { 338 | t.plan(8); 339 | 340 | var ee = new EventEmitter() 341 | , ns1 = cls.createNamespace('1') 342 | , ns2 = cls.createNamespace('2') 343 | ; 344 | 345 | // emulate an incoming data emitter 346 | setTimeout(function () { 347 | ee.emit('data', 'hi'); 348 | }, 10); 349 | 350 | t.doesNotThrow(function () { ns1.bindEmitter(ee); }); 351 | t.doesNotThrow(function () { ns2.bindEmitter(ee); }); 352 | 353 | ns1.run(function () { 354 | ns2.run(function () { 355 | ns1.set('name', 'tom1'); 356 | ns2.set('name', 'tom2'); 357 | 358 | t.doesNotThrow(function () { ns1.bindEmitter(ee); }); 359 | t.doesNotThrow(function () { ns2.bindEmitter(ee); }); 360 | 361 | ns1.run(function () { 362 | process.nextTick(function () { 363 | t.equal(ns1.get('name'), 'tom1', "ns1 value correct"); 364 | t.equal(ns2.get('name'), 'tom2', "ns2 value correct"); 365 | 366 | ns1.set('name', 'bob'); 367 | ns2.set('name', 'alice'); 368 | 369 | ee.on('data', function () { 370 | t.equal(ns1.get('name'), 'bob', "ns1 value bound onto emitter"); 371 | t.equal(ns2.get('name'), 'alice', "ns2 value bound onto emitter"); 372 | }); 373 | }); 374 | }); 375 | }); 376 | }); 377 | }); 378 | }); 379 | -------------------------------------------------------------------------------- /test/fs.tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createNamespace = require('../context.js').createNamespace 4 | , fs = require('fs') 5 | , path = require('path') 6 | , exec = require('child_process').exec 7 | , tap = require('tap') 8 | , test = tap.test 9 | ; 10 | 11 | // CONSTANTS 12 | var FILENAME = '__testfile' 13 | , DIRNAME = '__TESTDIR' 14 | , LINKNAME = '__testlink' 15 | , HARDLINKNAME = '__testhardlink' 16 | ; 17 | 18 | function createFile(assert) { 19 | var contents = new Buffer("UHOH") 20 | , file = fs.openSync(FILENAME, 'w') 21 | , written = fs.writeSync(file, contents, 0, contents.length, 0) 22 | ; 23 | assert.equals(written, contents.length, "whole buffer was written"); 24 | var rc = fs.closeSync(file); 25 | // need this here to avoid dealing with umask complications 26 | fs.chmodSync(FILENAME, '0666'); 27 | return rc; 28 | } 29 | 30 | function deleteFile() { return fs.unlinkSync(FILENAME); } 31 | 32 | 33 | function createLink(assert) { 34 | createFile(assert); 35 | fs.symlinkSync(FILENAME, LINKNAME); 36 | if (fs.lchmodSync) { 37 | // This function only exists on BSD systems (like OSX) 38 | fs.lchmodSync(LINKNAME, '0777'); 39 | } 40 | } 41 | 42 | function deleteLink() { 43 | fs.unlinkSync(LINKNAME); 44 | return deleteFile(); 45 | } 46 | 47 | 48 | function createDirectory(assert) { 49 | fs.mkdirSync(DIRNAME); 50 | assert.ok(fs.existsSync(DIRNAME), "directory was created"); 51 | } 52 | 53 | function deleteDirectory() { return fs.rmdirSync(DIRNAME); } 54 | 55 | 56 | function mapIds(username, groupname, callback) { 57 | if (!callback) throw new Error("mapIds requires callback"); 58 | if (!username) return callback(new Error("mapIds requires username")); 59 | if (!groupname) return callback(new Error("mapIds requires groupname")); 60 | 61 | exec('id -u ' + username, function (error, stdout, stderr) { 62 | if (error) return callback(error); 63 | if (stderr) return callback(new Error(stderr)); 64 | 65 | var uid = +stdout; 66 | exec('id -g ' + groupname, function (error, stdout, stderr) { 67 | if (error) return callback(error); 68 | if (stderr) return callback(new Error(stderr)); 69 | 70 | var gid = +stdout; 71 | callback(null, uid, gid); 72 | }); 73 | }); 74 | } 75 | 76 | test("continuation-local state with MakeCallback and fs module", function (t) { 77 | t.plan(33); 78 | 79 | var namespace = createNamespace('fs'); 80 | namespace.run(function () { 81 | namespace.set('test', 0xabad1dea); 82 | t.test("fs.rename", function (t) { 83 | createFile(t); 84 | 85 | namespace.run(function () { 86 | namespace.set('test', 'rename'); 87 | t.equal(namespace.get('test'), 'rename', "state has been mutated"); 88 | 89 | fs.rename(FILENAME, '__renamed', function (error) { 90 | t.notOk(error, "renaming shouldn't error"); 91 | t.equal(namespace.get('test'), 'rename', 92 | "mutated state has persisted to fs.rename's callback"); 93 | 94 | fs.unlinkSync('__renamed'); 95 | t.end(); 96 | }); 97 | }); 98 | }); 99 | 100 | t.test("fs.truncate", function (t) { 101 | // truncate -> ftruncate in Node > 0.8.x 102 | if (!fs.ftruncate) return t.end(); 103 | 104 | createFile(t); 105 | 106 | namespace.run(function () { 107 | namespace.set('test', 'truncate'); 108 | t.equal(namespace.get('test'), 'truncate', "state has been mutated"); 109 | 110 | fs.truncate(FILENAME, 0, function (error) { 111 | t.notOk(error, "truncation shouldn't error"); 112 | 113 | var stats = fs.statSync(FILENAME); 114 | t.equal(stats.size, 0, "file has been truncated"); 115 | 116 | t.equal(namespace.get('test'), 'truncate', 117 | "mutated state has persisted to fs.truncate's callback"); 118 | 119 | deleteFile(); 120 | t.end(); 121 | }); 122 | }); 123 | }); 124 | 125 | t.test("fs.ftruncate", function (t) { 126 | createFile(t); 127 | 128 | // truncate -> ftruncate in Node > 0.8.x 129 | var truncate = fs.ftruncate ? fs.ftruncate : fs.truncate; 130 | 131 | namespace.run(function () { 132 | namespace.set('test', 'ftruncate'); 133 | t.equal(namespace.get('test'), 'ftruncate', "state has been mutated"); 134 | 135 | var file = fs.openSync(FILENAME, 'w'); 136 | truncate(file, 0, function (error) { 137 | t.notOk(error, "truncation shouldn't error"); 138 | 139 | fs.closeSync(file); 140 | var stats = fs.statSync(FILENAME); 141 | t.equal(stats.size, 0, "file has been truncated"); 142 | 143 | t.equal(namespace.get('test'), 'ftruncate', 144 | "mutated state has persisted to fs.ftruncate's callback"); 145 | 146 | deleteFile(); 147 | t.end(); 148 | }); 149 | }); 150 | }); 151 | 152 | t.test("fs.chown", function (t) { 153 | createFile(t); 154 | 155 | mapIds('daemon', 'daemon', function (error, uid, gid) { 156 | t.notOk(error, "looking up uid & gid shouldn't error"); 157 | t.ok(uid, "uid for daemon was found"); 158 | t.ok(gid, "gid for daemon was found"); 159 | 160 | namespace.run(function () { 161 | namespace.set('test', 'chown'); 162 | t.equal(namespace.get('test'), 'chown', "state has been mutated"); 163 | 164 | fs.chown(FILENAME, uid, gid, function (error) { 165 | t.ok(error, "changing ownership will error for non-root users"); 166 | 167 | t.equal(namespace.get('test'), 'chown', 168 | "mutated state has persisted to fs.chown's callback"); 169 | 170 | deleteFile(); 171 | t.end(); 172 | }); 173 | }); 174 | }); 175 | }); 176 | 177 | t.test("fs.fchown", function (t) { 178 | createFile(t); 179 | 180 | mapIds('daemon', 'daemon', function (error, uid, gid) { 181 | t.notOk(error, "looking up uid & gid shouldn't error"); 182 | t.ok(uid, "uid for daemon was found"); 183 | t.ok(gid, "gid for daemon was found"); 184 | 185 | namespace.run(function () { 186 | namespace.set('test', 'fchown'); 187 | t.equal(namespace.get('test'), 'fchown', "state has been mutated"); 188 | 189 | var file = fs.openSync(FILENAME, 'w'); 190 | fs.fchown(file, uid, gid, function (error) { 191 | t.ok(error, "changing ownership will error for non-root users"); 192 | 193 | t.equal(namespace.get('test'), 'fchown', 194 | "mutated state has persisted to fs.fchown's callback"); 195 | 196 | fs.closeSync(file); 197 | deleteFile(); 198 | t.end(); 199 | }); 200 | }); 201 | }); 202 | }); 203 | 204 | t.test("fs.lchown", function (t) { 205 | if (!fs.lchown) return t.end(); 206 | createLink(t); 207 | 208 | mapIds('daemon', 'daemon', function (error, uid, gid) { 209 | t.notOk(error, "looking up uid & gid shouldn't error"); 210 | t.ok(uid, "uid for daemon was found"); 211 | t.ok(gid, "gid for daemon was found"); 212 | 213 | namespace.run(function () { 214 | namespace.set('test', 'lchown'); 215 | t.equal(namespace.get('test'), 'lchown', "state has been mutated"); 216 | 217 | fs.lchown(LINKNAME, uid, gid, function (error) { 218 | t.ok(error, "changing ownership will error for non-root users"); 219 | 220 | t.equal(namespace.get('test'), 'lchown', 221 | "mutated state has persisted to fs.lchown's callback"); 222 | 223 | deleteLink(); 224 | t.end(); 225 | }); 226 | }); 227 | }); 228 | }); 229 | 230 | t.test("fs.chmod", function (t) { 231 | createFile(t); 232 | 233 | namespace.run(function () { 234 | namespace.set('test', 'chmod'); 235 | t.equal(namespace.get('test'), 'chmod', "state has been mutated"); 236 | 237 | fs.chmod(FILENAME, '0700', function (error) { 238 | t.notOk(error, "changing mode shouldn't error"); 239 | 240 | t.equal(namespace.get('test'), 'chmod', 241 | "mutated state has persisted to fs.chmod's callback"); 242 | 243 | var stats = fs.statSync(FILENAME); 244 | t.equal(stats.mode.toString(8), '100700', "extra access bits are stripped"); 245 | 246 | deleteFile(); 247 | t.end(); 248 | }); 249 | }); 250 | }); 251 | 252 | t.test("fs.fchmod", function (t) { 253 | createFile(t); 254 | 255 | namespace.run(function () { 256 | namespace.set('test', 'fchmod'); 257 | t.equal(namespace.get('test'), 'fchmod', "state has been mutated"); 258 | 259 | var file = fs.openSync(FILENAME, 'w+'); 260 | fs.fchmod(file, '0700', function (error) { 261 | t.notOk(error, "changing mode shouldn't error"); 262 | 263 | t.equal(namespace.get('test'), 'fchmod', 264 | "mutated state has persisted to fs.fchmod's callback"); 265 | 266 | fs.closeSync(file); 267 | var stats = fs.statSync(FILENAME); 268 | t.equal(stats.mode.toString(8), '100700', "extra access bits are stripped"); 269 | 270 | deleteFile(); 271 | t.end(); 272 | }); 273 | }); 274 | }); 275 | 276 | t.test("fs.lchmod", function (t) { 277 | if (!fs.lchmod) return t.end(); 278 | createLink(t); 279 | 280 | namespace.run(function () { 281 | namespace.set('test', 'lchmod'); 282 | t.equal(namespace.get('test'), 'lchmod', "state has been mutated"); 283 | 284 | fs.lchmod(LINKNAME, '0700', function (error) { 285 | t.notOk(error, "changing mode shouldn't error"); 286 | 287 | t.equal(namespace.get('test'), 'lchmod', 288 | "mutated state has persisted to fs.lchmod's callback"); 289 | 290 | var stats = fs.lstatSync(LINKNAME); 291 | t.equal(stats.mode.toString(8), '120700', "extra access bits are stripped"); 292 | 293 | deleteLink(); 294 | t.end(); 295 | }); 296 | }); 297 | }); 298 | 299 | t.test("fs.stat", function (t) { 300 | createFile(t); 301 | 302 | namespace.run(function () { 303 | namespace.set('test', 'stat'); 304 | t.equal(namespace.get('test'), 'stat', "state has been mutated"); 305 | 306 | fs.stat(FILENAME, function (error, stats) { 307 | t.notOk(error, "reading stats shouldn't error"); 308 | 309 | t.equal(namespace.get('test'), 'stat', 310 | "mutated state has persisted to fs.stat's callback"); 311 | 312 | t.equal(stats.mode.toString(8), '100666', "permissions should be as created"); 313 | 314 | deleteFile(); 315 | t.end(); 316 | }); 317 | }); 318 | }); 319 | 320 | t.test("fs.fstat", function (t) { 321 | createFile(t); 322 | 323 | namespace.run(function () { 324 | namespace.set('test', 'fstat'); 325 | t.equal(namespace.get('test'), 'fstat', "state has been mutated"); 326 | 327 | var file = fs.openSync(FILENAME, 'r'); 328 | fs.fstat(file, function (error, stats) { 329 | t.notOk(error, "reading stats shouldn't error"); 330 | 331 | t.equal(namespace.get('test'), 'fstat', 332 | "mutated state has persisted to fs.fstat's callback"); 333 | 334 | t.equal(stats.mode.toString(8), '100666', "permissions should be as created"); 335 | 336 | fs.closeSync(file); 337 | deleteFile(); 338 | t.end(); 339 | }); 340 | }); 341 | }); 342 | 343 | t.test("fs.lstat", function (t) { 344 | createLink(t); 345 | 346 | namespace.run(function () { 347 | namespace.set('test', 'lstat'); 348 | t.equal(namespace.get('test'), 'lstat', "state has been mutated"); 349 | 350 | fs.lstat(LINKNAME, function (error, stats) { 351 | t.notOk(error, "reading stats shouldn't error"); 352 | 353 | t.equal(namespace.get('test'), 'lstat', 354 | "mutated state has persisted to fs.lstat's callback"); 355 | 356 | t.equal( 357 | stats.mode.toString(8), 358 | '120777', 359 | "permissions should be as created" 360 | ); 361 | 362 | deleteLink(); 363 | t.end(); 364 | }); 365 | }); 366 | }); 367 | 368 | t.test("fs.link", function (t) { 369 | createFile(t); 370 | 371 | namespace.run(function () { 372 | namespace.set('test', 'link'); 373 | t.equal(namespace.get('test'), 'link', "state has been mutated"); 374 | 375 | fs.link(FILENAME, HARDLINKNAME, function (error) { 376 | t.notOk(error, "creating a link shouldn't error"); 377 | 378 | t.equal(namespace.get('test'), 'link', 379 | "mutated state has persisted to fs.link's callback"); 380 | 381 | var orig = fs.statSync(FILENAME) 382 | , linked = fs.statSync(HARDLINKNAME) 383 | ; 384 | t.equal(orig.ino, linked.ino, "entries should point to same file"); 385 | 386 | t.notOk(fs.unlinkSync(HARDLINKNAME), "link has been removed"); 387 | deleteFile(); 388 | t.end(); 389 | }); 390 | }); 391 | }); 392 | 393 | t.test("fs.symlink", function (t) { 394 | createFile(t); 395 | 396 | namespace.run(function () { 397 | namespace.set('test', 'symlink'); 398 | t.equal(namespace.get('test'), 'symlink', "state has been mutated"); 399 | 400 | fs.symlink(FILENAME, LINKNAME, function (error) { 401 | t.notOk(error, "creating a symlink shouldn't error"); 402 | 403 | t.equal(namespace.get('test'), 'symlink', 404 | "mutated state has persisted to fs.symlink's callback"); 405 | 406 | var pointed = fs.readlinkSync(LINKNAME); 407 | t.equal(pointed, FILENAME, "symlink points back to original file"); 408 | 409 | t.notOk(fs.unlinkSync(LINKNAME), "symlink has been removed"); 410 | deleteFile(); 411 | t.end(); 412 | }); 413 | }); 414 | }); 415 | 416 | t.test("fs.readlink", function (t) { 417 | createLink(t); 418 | 419 | namespace.run(function () { 420 | namespace.set('test', 'readlink'); 421 | t.equal(namespace.get('test'), 'readlink', "state has been mutated"); 422 | 423 | fs.readlink(LINKNAME, function (error, pointed) { 424 | t.notOk(error, "reading symlink shouldn't error"); 425 | 426 | t.equal(namespace.get('test'), 'readlink', 427 | "mutated state has persisted to fs.readlink's callback"); 428 | 429 | t.equal(pointed, FILENAME, "symlink points back to original file"); 430 | 431 | deleteLink(); 432 | t.end(); 433 | }); 434 | }); 435 | }); 436 | 437 | t.test("fs.unlink", function (t) { 438 | createFile(t); 439 | 440 | namespace.run(function () { 441 | namespace.set('test', 'unlink'); 442 | t.equal(namespace.get('test'), 'unlink', "state has been mutated"); 443 | 444 | fs.unlink(FILENAME, function (error) { 445 | t.notOk(error, "deleting file shouldn't error"); 446 | 447 | t.equal(namespace.get('test'), 'unlink', 448 | "mutated state has persisted to fs.unlink's callback"); 449 | 450 | t.notOk(fs.exists(FILENAME), "file should be gone"); 451 | t.end(); 452 | }); 453 | }); 454 | }); 455 | 456 | t.test("fs.realpath", function (t) { 457 | createFile(t); 458 | 459 | namespace.run(function () { 460 | namespace.set('test', 'realpath'); 461 | t.equal(namespace.get('test'), 'realpath', "state has been mutated"); 462 | 463 | fs.realpath(FILENAME, function (error, real) { 464 | t.notOk(error, "deleting file shouldn't error"); 465 | 466 | t.equal(namespace.get('test'), 'realpath', 467 | "mutated state has persisted to fs.realpath's callback"); 468 | 469 | t.equal(real, path.resolve(FILENAME), "no funny business with the real path"); 470 | 471 | deleteFile(); 472 | t.end(); 473 | }); 474 | }); 475 | }); 476 | 477 | t.test("fs.mkdir", function (t) { 478 | namespace.run(function () { 479 | namespace.set('test', 'mkdir'); 480 | t.equal(namespace.get('test'), 'mkdir', "state has been mutated"); 481 | 482 | fs.mkdir(DIRNAME, function (error) { 483 | t.notOk(error, "creating directory shouldn't error"); 484 | 485 | t.equal(namespace.get('test'), 'mkdir', 486 | "mutated state has persisted to fs.mkdir's callback"); 487 | 488 | t.ok(fs.existsSync(DIRNAME), "directory was created"); 489 | 490 | fs.rmdirSync(DIRNAME); 491 | t.end(); 492 | }); 493 | }); 494 | }); 495 | 496 | t.test("fs.rmdir", function (t) { 497 | createDirectory(t); 498 | 499 | namespace.run(function () { 500 | namespace.set('test', 'rmdir'); 501 | t.equal(namespace.get('test'), 'rmdir', "state has been mutated"); 502 | 503 | fs.rmdir(DIRNAME, function (error) { 504 | t.notOk(error, "deleting directory shouldn't error"); 505 | 506 | t.equal(namespace.get('test'), 'rmdir', 507 | "mutated state has persisted to fs.rmdir's callback"); 508 | 509 | t.notOk(fs.existsSync(DIRNAME), "directory was removed"); 510 | 511 | t.end(); 512 | }); 513 | }); 514 | }); 515 | 516 | t.test("fs.readdir", function (t) { 517 | createDirectory(t); 518 | 519 | var file1 = fs.openSync(path.join(DIRNAME, 'file1'), 'w'); 520 | fs.writeSync(file1, 'one'); 521 | fs.closeSync(file1); 522 | 523 | var file2 = fs.openSync(path.join(DIRNAME, 'file2'), 'w'); 524 | fs.writeSync(file2, 'two'); 525 | fs.closeSync(file2); 526 | 527 | var file3 = fs.openSync(path.join(DIRNAME, 'file3'), 'w'); 528 | fs.writeSync(file3, 'three'); 529 | fs.closeSync(file3); 530 | 531 | namespace.run(function () { 532 | namespace.set('test', 'readdir'); 533 | t.equal(namespace.get('test'), 'readdir', "state has been mutated"); 534 | 535 | fs.readdir(DIRNAME, function (error, contents) { 536 | t.notOk(error, "reading directory shouldn't error"); 537 | 538 | t.equal(namespace.get('test'), 'readdir', 539 | "mutated state has persisted to fs.readdir's callback"); 540 | 541 | t.equal(contents.length, 3, "3 files were found"); 542 | 543 | fs.unlinkSync(path.join(DIRNAME, 'file1')); 544 | fs.unlinkSync(path.join(DIRNAME, 'file2')); 545 | fs.unlinkSync(path.join(DIRNAME, 'file3')); 546 | deleteDirectory(); 547 | t.end(); 548 | }); 549 | }); 550 | }); 551 | 552 | t.test("fs.watch", function (t) { 553 | createFile(t); 554 | 555 | namespace.run(function () { 556 | namespace.set('test', 'watch'); 557 | t.equal(namespace.get('test'), 'watch', "state has been mutated"); 558 | 559 | var watcher = fs.watch(FILENAME, 560 | {persistent : false, interval : 200}, 561 | function (event) { 562 | t.equal(namespace.get('test'), 'watch', 563 | "mutated state has persisted to fs.watch's callback"); 564 | 565 | t.equal(event, 'change', "file was changed"); 566 | 567 | watcher.close(); 568 | process.nextTick(function cleanup() { 569 | deleteFile(); 570 | t.end(); 571 | }); 572 | }); 573 | 574 | setTimeout(function poke() { 575 | fs.writeFileSync(FILENAME, 'still a test'); 576 | }, 20); 577 | }); 578 | }); 579 | 580 | t.test("fs.utimes", function (t) { 581 | createFile(t); 582 | 583 | /* utimes(2) takes seconds since the epoch, and Date() deals with 584 | * milliseconds. I just want a date some time in the past. 585 | */ 586 | var PASTIME = new Date(Math.floor((Date.now() - 31337) / 1000) * 1000); 587 | 588 | namespace.run(function () { 589 | namespace.set('test', 'utimes'); 590 | t.equal(namespace.get('test'), 'utimes', "state has been mutated"); 591 | 592 | var before = fs.statSync(FILENAME); 593 | t.ok(before.atime, "access time of newly-created file set"); 594 | t.ok(before.mtime, "modification time of newly-created file set"); 595 | 596 | fs.utimes(FILENAME, PASTIME, PASTIME, function (error) { 597 | t.notOk(error, "setting utimes shouldn't error"); 598 | 599 | t.equal(namespace.get('test'), 'utimes', 600 | "mutated state has persisted to fs.utimes's callback"); 601 | 602 | var after = fs.statSync(FILENAME); 603 | t.deepEqual(after.atime, PASTIME, "access time of newly-created file is reset"); 604 | t.deepEqual(after.mtime, PASTIME, "mod time of newly-created file is reset"); 605 | t.notDeepEqual(before.atime, after.atime, "access time changed"); 606 | t.notDeepEqual(before.mtime, after.mtime, "mod time changed"); 607 | 608 | deleteFile(); 609 | t.end(); 610 | }); 611 | }); 612 | }); 613 | 614 | t.test("fs.futimes", function (t) { 615 | createFile(t); 616 | 617 | /* futimes(2) takes seconds since the epoch, and Date() deals with 618 | * milliseconds. I just want a date some time in the past. 619 | */ 620 | var PASTIME = new Date(Math.floor((Date.now() - 0xb33fd) / 1000) * 1000); 621 | 622 | namespace.run(function () { 623 | namespace.set('test', 'futimes'); 624 | t.equal(namespace.get('test'), 'futimes', "state has been mutated"); 625 | 626 | var before = fs.statSync(FILENAME); 627 | t.ok(before.atime, "access time of newly-created file set"); 628 | t.ok(before.mtime, "modification time of newly-created file set"); 629 | 630 | var file = fs.openSync(FILENAME, "w+"); 631 | fs.futimes(file, PASTIME, PASTIME, function (error) { 632 | t.notOk(error, "setting futimes shouldn't error"); 633 | fs.closeSync(file); 634 | 635 | t.equal(namespace.get('test'), 'futimes', 636 | "mutated state has persisted to fs.futimes's callback"); 637 | 638 | var after = fs.statSync(FILENAME); 639 | t.deepEqual(after.atime, PASTIME, "access time of newly-created file is reset"); 640 | t.deepEqual(after.mtime, PASTIME, "mod time of newly-created file is reset"); 641 | t.notDeepEqual(before.atime, after.atime, "access time changed"); 642 | t.notDeepEqual(before.mtime, after.mtime, "mod time changed"); 643 | 644 | deleteFile(); 645 | t.end(); 646 | }); 647 | }); 648 | }); 649 | 650 | t.test("fs.fsync", function (t) { 651 | createFile(t); 652 | 653 | namespace.run(function () { 654 | namespace.set('test', 'fsync'); 655 | t.equal(namespace.get('test'), 'fsync', "state has been mutated"); 656 | 657 | var file = fs.openSync(FILENAME, 'w+'); 658 | fs.fsync(file, function (error) { 659 | t.notOk(error, "syncing the file shouldn't error"); 660 | 661 | t.equal(namespace.get('test'), 'fsync', 662 | "mutated state has persisted to fs.fsync's callback"); 663 | 664 | fs.closeSync(file); 665 | deleteFile(); 666 | t.end(); 667 | }); 668 | }); 669 | }); 670 | 671 | t.test("fs.open", function (t) { 672 | createFile(t); 673 | 674 | namespace.run(function () { 675 | namespace.set('test', 'open'); 676 | t.equal(namespace.get('test'), 'open', "state has been mutated"); 677 | 678 | fs.open(FILENAME, 'r', function (error, file) { 679 | t.notOk(error, "opening the file shouldn't error"); 680 | 681 | t.equal(namespace.get('test'), 'open', 682 | "mutated state has persisted to fs.open's callback"); 683 | 684 | 685 | var contents = new Buffer(4); 686 | fs.readSync(file, contents, 0, 4, 0); 687 | t.equal(contents.toString(), 'UHOH', "contents are still available"); 688 | 689 | fs.closeSync(file); 690 | deleteFile(); 691 | t.end(); 692 | }); 693 | }); 694 | }); 695 | 696 | t.test("fs.close", function (t) { 697 | createFile(t); 698 | 699 | namespace.run(function () { 700 | namespace.set('test', 'close'); 701 | t.equal(namespace.get('test'), 'close', "state has been mutated"); 702 | 703 | var file = fs.openSync(FILENAME, 'r'); 704 | fs.close(file, function (error) { 705 | t.notOk(error, "closing the file shouldn't error"); 706 | 707 | t.equal(namespace.get('test'), 'close', 708 | "mutated state has persisted to fs.close's callback"); 709 | 710 | deleteFile(); 711 | t.end(); 712 | }); 713 | }); 714 | }); 715 | 716 | t.test("fs.read", function (t) { 717 | createFile(t); 718 | 719 | namespace.run(function () { 720 | namespace.set('test', 'read'); 721 | t.equal(namespace.get('test'), 'read', "state has been mutated"); 722 | 723 | var file = fs.openSync(FILENAME, 'r') 724 | , contents = new Buffer(4) 725 | ; 726 | fs.read(file, contents, 0, 4, 0, function (error) { 727 | t.notOk(error, "reading from the file shouldn't error"); 728 | 729 | t.equal(namespace.get('test'), 'read', 730 | "mutated state has persisted to fs.read's callback"); 731 | 732 | t.equal(contents.toString(), 'UHOH', "contents are still available"); 733 | 734 | fs.closeSync(file); 735 | deleteFile(); 736 | t.end(); 737 | }); 738 | }); 739 | }); 740 | 741 | t.test("fs.write", function (t) { 742 | createFile(t); 743 | 744 | namespace.run(function () { 745 | namespace.set('test', 'write'); 746 | t.equal(namespace.get('test'), 'write', "state has been mutated"); 747 | 748 | var file = fs.openSync(FILENAME, 'w') 749 | , contents = new Buffer('yeap') 750 | ; 751 | fs.write(file, contents, 0, 4, 0, function (error) { 752 | t.notOk(error, "writing to the file shouldn't error"); 753 | 754 | t.equal(namespace.get('test'), 'write', 755 | "mutated state has persisted to fs.write's callback"); 756 | 757 | fs.closeSync(file); 758 | 759 | var readback = fs.readFileSync(FILENAME); 760 | t.equal(readback.toString(), 'yeap', "contents are still available"); 761 | 762 | deleteFile(); 763 | t.end(); 764 | }); 765 | }); 766 | }); 767 | 768 | t.test("fs.readFile", function (t) { 769 | createFile(t); 770 | 771 | namespace.run(function () { 772 | namespace.set('test', 'readFile'); 773 | t.equal(namespace.get('test'), 'readFile', "state has been mutated"); 774 | 775 | fs.readFile(FILENAME, function (error, contents) { 776 | t.notOk(error, "reading from the file shouldn't error"); 777 | 778 | t.equal(namespace.get('test'), 'readFile', 779 | "mutated state has persisted to fs.readFile's callback"); 780 | 781 | t.equal(contents.toString(), 'UHOH', "contents are still available"); 782 | 783 | deleteFile(); 784 | t.end(); 785 | }); 786 | }); 787 | }); 788 | 789 | t.test("fs.writeFile", function (t) { 790 | createFile(t); 791 | 792 | namespace.run(function () { 793 | namespace.set('test', 'writeFile'); 794 | t.equal(namespace.get('test'), 'writeFile', "state has been mutated"); 795 | 796 | fs.writeFile(FILENAME, 'woopwoop', function (error) { 797 | t.notOk(error, "rewriting the file shouldn't error"); 798 | 799 | t.equal(namespace.get('test'), 'writeFile', 800 | "mutated state has persisted to fs.writeFile's callback"); 801 | 802 | var readback = fs.readFileSync(FILENAME); 803 | t.equal(readback.toString(), 'woopwoop', "rewritten contents are available"); 804 | 805 | deleteFile(); 806 | t.end(); 807 | }); 808 | }); 809 | }); 810 | 811 | t.test("fs.appendFile", function (t) { 812 | createFile(t); 813 | 814 | namespace.run(function () { 815 | namespace.set('test', 'appendFile'); 816 | t.equal(namespace.get('test'), 'appendFile', "state has been mutated"); 817 | 818 | fs.appendFile(FILENAME, '/jk', function (error) { 819 | t.notOk(error, "appending to the file shouldn't error"); 820 | 821 | t.equal(namespace.get('test'), 'appendFile', 822 | "mutated state has persisted to fs.appendFile's callback"); 823 | 824 | var readback = fs.readFileSync(FILENAME); 825 | t.equal(readback.toString(), 'UHOH/jk', 826 | "appended contents are still available"); 827 | 828 | deleteFile(); 829 | t.end(); 830 | }); 831 | }); 832 | }); 833 | 834 | t.test("fs.exists", function (t) { 835 | createFile(t); 836 | 837 | namespace.run(function () { 838 | namespace.set('test', 'exists'); 839 | t.equal(namespace.get('test'), 'exists', "state has been mutated"); 840 | 841 | fs.exists(FILENAME, function (yep) { 842 | t.equal(namespace.get('test'), 'exists', 843 | "mutated state has persisted to fs.exists's callback"); 844 | 845 | t.ok(yep, "precreated file does indeed exist."); 846 | 847 | fs.exists('NOPENOWAY', function (nope) { 848 | t.equal(namespace.get('test'), 'exists', 849 | "mutated state continues to persist to fs.exists's second callback"); 850 | 851 | t.notOk(nope, "nonexistent file doesn't exist."); 852 | 853 | deleteFile(); 854 | t.end(); 855 | }); 856 | }); 857 | }); 858 | }); 859 | 860 | t.test("fs.watchFile", function (t) { 861 | createFile(t); 862 | 863 | namespace.run(function () { 864 | namespace.set('test', 'watchFile'); 865 | t.equal(namespace.get('test'), 'watchFile', "state has been mutated"); 866 | 867 | var options = { 868 | persistent: true, 869 | interval: 20 870 | }; 871 | 872 | fs.watchFile(FILENAME, options, function (before, after) { 873 | t.equal(namespace.get('test'), 'watchFile', 874 | "mutated state has persisted to fs.watchFile's callback"); 875 | 876 | t.ok(before.ino, "file has an entry"); 877 | t.equal(before.ino, after.ino, "file is at the same location"); 878 | 879 | fs.unwatchFile(FILENAME); 880 | process.nextTick(function () { 881 | deleteFile(); 882 | t.end(); 883 | }); 884 | }); 885 | 886 | setTimeout(function poke() { 887 | fs.appendFileSync(FILENAME, 'still a test'); 888 | }, 20); 889 | }); 890 | }); 891 | }); 892 | }); 893 | --------------------------------------------------------------------------------